GatsbyJS: How to persist layout and keep state between pages with createContext and useReducer
If you use Gatsby v2, there is no longer one standard layout component that gets automatically wrapped around every generated page. Instead, the layout-component has to be included in every created page, making an easy manipulation of page-specific layout changes possible (and is in line with the normal component composition model of React).
There are a couple of drawbacks though: The state of the layout is reset with every page and transitions are harder to execute as the entire page unmounts and remounts.
Plugin: gatsby-plugin-layout
To regain the global layout component from Gatsby v1 we need the official plugin ”gatsby-plugin-layout“.
Install it with:
npm install gatsby-plugin-layout --save
Then we add it to the gatsby-config.js
:
module.exports = {
plugins: [`gatsby-plugin-layout`],
}
As per default the plugin expects your layout file to sit in src/layouts/index.js
. You can change that in the gatsby-config.js
:
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-layout`,
options: {
component: require.resolve(`./some/other/path/component`),
},
},
],
}
The layout component
The minimal viable layout component looks like this:
/* layouts/index.js */
import React from "react"
const Layout = ({ children }) => {
return (
<>
{/*Header*/}
{children}
{/*Footer*/}
</>
)
}
export default Layout
This gives you a truly global component, where a header or a footer that doesn’t re-render between page changes can reside.
Keeping state within the layout component
If you just have to keep state in the layout and its components, you can just use the useState
hook. In this example a boolean determines if the menu is open:
/* layouts/index.js */
import React, { useState } from "react"
import { Link } from "gatsby"
const Layout = ({ children }) => {
const [menuOpen, setMenuOpen] = useState(false)
return (
<>
{/*Header*/}
{menuOpen && <Menu />}
<button
onClick={() => {
setMenuOpen(!menuOpen)
}}
>
Toggle menu
</button>
{children}
{/*Footer*/}
</>
)
}
export default Layout
const Menu = () => {
return (
<ul className="menu">
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
)
}
Sharing state between the layout component and nested pages
Now this is where it gets interesting.
To share state between layout and nested pages, we use the createContext
hook to keep and share our state, as well as the useReducer
hook to set it.
The main work is done in a separate file for our Context. I store this directly in the root folder, but you can place it wherever you want. It makes use of createContext
and useReducer
like so:
/* ./GlobalContext.js */
import React, { createContext } from "react"
// First, we have to prepare some data and functions, that we're later gonna use
// in our reducer
const initialState = {
openMenu: false,
}
const actions = {
SET_MENU: "SET_MENU",
}
const reducer = (state, action) => {
switch (action.type) {
case actions.SET_MENU:
return { ...state, openMenu: action.value }
default:
return state
}
}
// Then we are creating a context out of the scope of the function
// "GlobalContextProvider" so that we can export it
// and use it in our pages
const GlobalContext = createContext()
const GlobalContextProvider = ({ children }) => {
// Within our overarching component, we now create a React-Reducer with the data that
// we've prepared. This gives us a current state and a dispatch function to
// invoke an action of the reducer function declared earlier.
const [state, dispatch] = React.useReducer(reducer, initialState)
// Equipped with "state" and "dispatch" we can now create a value-object, that goes
// into our ContextProvider.
const value = {
openMenu: state.openMenu,
setOpenMenu: value => {
dispatch({ type: actions.SET_MENU, value })
},
}
return (
<GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>
)
}
// The exported GlobalContextProvider will only be used in our layout/index.js,
// while the general GlobalContext can be used by any Page or Component (with
// the help of useContext).
export { GlobalContextProvider as default, GlobalContext }
Let’s now return to our layouts/index.js
file, where we implement the GlobalContextProvider
, simply wrapping everything that we return in it:
/* layouts/index.js */
import React from "react"
import GlobalContextProvider from "./GlobalContext.js"
const Layout = ({ children }) => {
return (
<GlobalContextProvider>
{/*Header*/}
{children}
{/*Footer*/}
</GlobalContextProvider>
)
}
export default Layout
As an example, lets now set the menu-state to true within our Gatsby-page “index.js” (not layout/index.js !):
/*./src/pages/index.js*/
import React, { useContext } from "react"
import { Link } from "gatsby"
// Lets import our GlobalContext
import { GlobalContext } from "../../GlobalContext.js"
const IndexPage = () => {
// We interpret our current context with useContext
// and get our "openMenu" state and our "setOpenMenu" function from it.
const { openMenu, setOpenMenu } = useContext(GlobalContext)
return (
<>
<button
onClick={() => {
setOpenMenu(!openMenu)
}}
>
Toggle Menu State
</button>
<p>The menu state is currently {openMenu ? `true` : `false`}.</p>
<Link to="/page-2/">Go to page 2</Link> <br />
</>
)
}
export default IndexPage
On page-2
we can now read the current state of the menu in the exact same way:
/*./src/pages/page-2.js*/
import React from "react"
import { Link } from "gatsby"
// We need again our GlobalContext
import { GlobalContext } from "../../GlobalContext.js"
const SecondPage = () => {
//And we have to use the Global Context the exact same way.
const { openMenu } = React.useContext(GlobalContext)
return (
<>
<p>Welcome to page 2</p>
<p>The menu state is currently {openMenu ? `true` : `false`}.</p>
<Link to="/">Go back to the homepage</Link>
</>
)
}
export default SecondPage
You can explore a working example at: codesandbox.io/s/stupefied-colden-oeibg
If you want a deeper dive on how to share state via the createContext
-hook and how to work with useReducer
, i’d suggest you have a gander at the video-tutorial by Leigh Halliday.
Recapping
What we’ve done here:
- We’ve re-implemented the old layout-functionality from Gatsby v1, using the Gatsby plugin
gatsby-plugin-layout
. - We can now use a layout component that Gatsby wraps automatically around every generated page to wrap elements around our pages that dont re-render on every page change.
- If we want to store state only available to this layout component, we can use the known methods of
useState
or else. - If we want to make state available to all components including the rendered pages of Gatsby, we have to create an external context with an reducer that helps us set a new state. We can import this in every component on every page.
Hope that this helped you! That’s it for today! Have a good one!