Hooking into Gatsby's navigation changes

Sunday, July 19, 2020

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.

1// 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.

1// 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.

1// 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.

1// 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.


Other posts


Tagged with: