Hooking into Gatsby's navigation changes

On my site there is a toggling navigation menu. But when I toggle it open and then click a link it stays open. This is not right - it should close when you change location. Let's fix it using some really simple hooks in a few minutes. First lets see what we have to start with.

// Original basic version of my Nav component
import React, { useState } from "react";

const Nav = ({ nav }) => {
    const [open, setOpen] = useState(false);

    return (
      <div>
       <button
          type="button"
          onClick={() => {
              setOpen(!open);
          }}
      >
        Toggle
      </button>
        <div className={`${open ? "show" : null}`}>
          <Link to="/">Home</Link>
          <Link to="/about/">About</Link>
        </div>
      </div>
    )
}

This will let the user toggle the nav and see the links by toggling setOpen. They can then navigate to a new page, but because the Nav exists higher up the nesting it never gets re-mounted so open stays in the same state which is true - which is open.

We need to hook into navigation changes - these are from the router and (luckily) are in the context sent to the page. Let's pull out the data.

// Now with some location awareness
import React, { useState } from "react";
import { useLocation } from "@reach/router"; // NEW

const Nav = ({ nav }) => {
    const [open, setOpen] = useState(false);
    const location = useLocation(); // NEW

    return (
      <div>
       <button
          type="button"
          onClick={() => {
              setOpen(!open);
          }}
      >
        Toggle
      </button>
        <div className={`${open ? "show" : null}`}>
          <Link to="/">Home</Link>
          <Link to="/about/">About</Link>
        </div>
      </div>
    )
}

The component is now aware of the location, but had no lasting memory so it does not know if anything has changed. Luckily we can use a hook to track that.

// Now with some location awareness
import React, { useState, useRef } from "react"; // UPDATED
import { useLocation } from "@reach/router";

// New hook
const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
};

const Nav = ({ nav }) => {
    const [open, setOpen] = useState(false);
    const location = useLocation(); 
    const prevLocation = usePrevious(location); // NEW

    return (
      <div>
       <button
          type="button"
          onClick={() => {
              setOpen(!open);
          }}
      >
        Toggle
      </button>
        <div className={`${open ? "show" : null}`}>
          <Link to="/">Home</Link>
          <Link to="/about/">About</Link>
        </div>
      </div>
    )
}

Now we have the current and previous locations it's a simple step to compare them. To make sure this only happens when the location changes we will use a useEffect() and set the inputs to location and prevLocation.

// Now with some location awareness
import React, { useState, useRef, useEffect } from "react"; // UPDATED
import { useLocation } from "@reach/router";

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
};

const Nav = ({ nav }) => {
    const [open, setOpen] = useState(false);
    const location = useLocation(); 
    const prevLocation = usePrevious(location);

    // NEW
    useEffect(() => {
        if (location !== prevLocation) {
            setOpen(false);
        }
    }, [location, prevLocation, setOpen]);
    
    return (
      <div>
       <button
          type="button"
          onClick={() => {
              setOpen(!open);
          }}
      >
        Toggle
      </button>
        <div className={`${open ? "show" : null}`}>
          <Link to="/">Home</Link>
          <Link to="/about/">About</Link>
        </div>
      </div>
    )
}

With the final update the Nav will now "listen" to location changes. When it changes and the location does not equal the previous one we set the state to false, closing the toggled menu.