allow component to detect route change in Gatsby

In a Gatsby project I have a header component which is persistent on every page. The header has a modal to display the navigation. I need to set the isOpen state to false whenever the route changes so that the nav modal closes. Since the route can change not just by clicking links in the modal but also by using the back button on the browser I don’t want use an event on the links to close the modal.

In Gatsby I can use the onRouteUpdate in gatsby-browser.js to detect route changes and this works well. But I need to pass the event to my component and this is where I am having difficulty. I have simplified the code below to show the setup.

gatsby-browser.js:

import React from "react"
import Layout from "./src/components/layout"

export const wrapPageElement = ({ element, props }) => {
  return <Layout {...props}>{element}</Layout>
}

export const onRouteUpdate = () => {
  console.log("onRouteUpdate") // this works
}

layout.js:

import React from "react"

import Header from "./header"
import Footer from "./footer"

const Layout = ({ children }) => (
    <>
      <Header />
      <main>
        {children}
      </main>
      <Footer />
    </>
  )

export default Layout

header.js:

import React, { useState } from "react"

const Header = () => {
  const [isOpen, setIsOpen] = useState(null)
  const toggleState = ({ props }) => {
    let status
    if (props) status = props.status
    else status = !isOpen
    setIsOpen(status)
  }
  return (
    <header>
      <div>This is the header</div>
      <button onClick={toggleState}>Toggle Open/Close</button>
      <button onClick={toggleState({ status: false })}>This will always close</button>
      /* logic here uses isOpen state to determine display */
    </header>
  )
}

export default Header

Answer

I have come up with a solution to my own question so I thought I would share. Any comments/improvements are always welcome.

First, we do not need to use “onRouteUpdate” in gatsby-broser.js so let’s remove that:

/* gatsby-browser.js */
import React from "react"
import Layout from "./src/components/layout"

export const wrapPageElement = ({ element, props }) => {
  return <Layout {...props}>{element}</Layout>
}

Then, in layout.js make sure to pass the location to the header:

/* layout.js */
import React from "react"

import Header from "./header"
import Footer from "./footer"

const Layout = ({ children, location }) => (
    <>
      <Header location={location} />
      <main>
        {children}
      </main>
      <Footer />
    </>
  )

export default Layout

Finally, in header.js the location is stored on a reference to the header element by utilizing the useRef hook. The useEffect hook will get fired on route changes so we can use that to compare:

/* header.js */
import React, { useState, useEffect, useRef } from "react"

const Header = () => {
  const [isOpen, setIsOpen] = useState(null)
  const myRef = useRef({
    location: null,
  })
  useEffect(() => {
    // set the location on initial load
    if (!myRef.current.location) myRef.current.location = location
    // then make sure dialog is closed on route change
    else if (myRef.current.location !== location) {
      if (isOpen) toggleState({ status: false })
      myRef.current.location = location
    }
  })
  const toggleState = ({ props }) => {
    let status
    if (props) status = props.status
    else status = !isOpen
    setIsOpen(status)
  }
  return (
    <header ref={myRef}>
      <div>This is the header</div>
      <button onClick={toggleState}>Toggle Open/Close</button>
      <button onClick={toggleState({ status: false })}>This will always close</button>
    </header>
  )
}

export default Header

Hopefully this helps anyone looking for similar functionality.