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
My preferred approach to solving this is to use the undocumented globalHistory
from @reach/router
, which Gatsby uses.
import { globalHistory } from '@reach/router'
useEffect(() => {
return globalHistory.listen(({ action }) => {
if (action === 'PUSH') setIsOpen(false)
})
}, [setIsOpen])
Now whenever you switch routes, the above effect will fire.
Source.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With