Hooking into Gatsby's navigation changes
Jul 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.