How to persist state between pages - Gatsby with Context

September 04, 2020

Have you ever wondered how to make your states persis between pages? You’re in the right place! I’ll teach you how to do just that, but with some limitations - this will not persist through refresh, bummer I know.

We’ll do this with an example of a color theme, dark vs light, using React’s Context API. The first step is to setup a Provider with our isActive state as well as a way to toggle its current state. We’ll be using props and wrap them in myContext.Provider. Go ahead and make a components/provider.js file. Instead of exporting Provider itself, we’ll be exporting a function that will wrap whatever is passed to it in our Provider.

components/provider.js
import React, { useState } from 'react';

export const myContext = React.createContext();

const Provider = props => {
  const [isActive, setTheme] = useState(false);

  return (
    <myContext.Provider value={{
      isActive,
      changeTheme: () => setTheme(!isActive)
    }}>
      {props.children}
    </myContext.Provider>
  )
};

export default ({ element }) => (
  <Provider>
    {element}
  </Provider>
);

That’s pretty much all the heavy lifting we need to do when it comes to managing our state! All that’s left is wrapping our pages so the state can flow. Gatsby is pretty cool and it gives us access to a hook called wrapRootElement - you can read more in depth about it here. But it basically takes everything in our site and passes it as props into the function we exported in our Provider!

Your gatsby-browser.js file has access to this hook, we’ll be wrapping it up with our Provider. It’s recommended to also add this to your gatsby-ssr.js file, if you have it.

gatsby-browser.js
import "./src/styles/theme.scss"
import Provider from './provider';

export const wrapRootElement = Provider;

Now, if you have some basic stylings already in place for both themes, you’re about ready to see the toggle in action! If not, go ahead and create some in your CSS/Sass. Keep note of the light and dark class names you choose. I chose to import my styles into the gatsby-browser.js file.

Let’s Apply It!

Finally, the last thing we need to do to access our state throughout our pages is to wrap each page’s content in a myContext.Consumer tag and make use of our global state context. I’ve included React.Fragment in this example, because it allows us to add more than one element, like we would usually use an Array for.

Also, before we forget, in order to actually toggle the theme, we’d need some sort of button with our toggle action. Make sure you add onClick={() => context.changeTheme()} to your chosen button or link. This references our changeTheme: () => setTheme(!isDark) function that we defined back in our Provider.

pages/index.js
import { myContext } from '../../provider';

const BlogIndex = ({ data, location }) => {
  
  return (
    <myContext.Consumer>
      {context => (
        <React.Fragment>
          <div>
            <button className="theme-toggle" onClick={() => context.changeTheme()}>Click Me!</button>
          </div>
          <div>
            <h2>Second element</h2>
          </div>
        </React.Fragment>
      )}
    </myContext.Consumer>
  )
}

export default BlogIndex

You’ll want to setup the same configuration to all your pages, wrapping their main content as we did above, this is what will make the state persist while moving between them! Nifty, right?

Body class or wrapper class?

You have an option here, if you’d like your class added to your body element, you’ll make the following addition to your provider.js file:

components/provider.js
const Provider = props => {
  const [isActive, setTheme] = useState(false);
  document.body.className = isActive ? 'dark' : 'light';

document.body.className = isActive ? 'dark' : 'light'; will add your class directly to the body element. If you want to add this class to another element, say you’re not even using this for theme management, you’ll instead add className={context.isActive ? 'dark' : 'light'} to whatever element you’d like, given it is wrapped inside <myContext.Consumer> in your JSX!

Fin!


© 2021, Built with ❤️️