Managing global state in Gatsby

thumbnail

Using the useContext and wrapRootElement hooks

Neo
Wednesday, August 26, 2020

Motivation

As someone who spends a lot of time reading on the web, I wanted to add a simple toggle to my own blog to switch between ‘light’ and ‘dark’ themes, as this isn’t a feature that all mobile browsers support just yet.

While is relatively simple to use state to set a component’s styling from a button press, it’s not as simple to have the state persist when you navigate to a different page on your site.

After considering some different ideas on how to accomplish this, I came across the following solution and implemented it within this blog.

The solution is basically a high-order Provider component which allows setting/getting of a Context from anywhere within your application using useContext.

This could be useful for other Gatsby developers out there who have a need to implement a simple global state that is persistent across page changes (but not page refreshes), such as language, currency, time zones, or other types of accessibility settings.

What’s Involved

  • Creating a Context with React.createContext
  • Creating a high-order Provider component and adding state with React.useState
  • Adding your new Provider component to gatsby-ssr.js and gatsby-browser.js
  • Setting and consuming the global Context

Create a Context with createContext

For my project, I created a ThemeProvider.js with the following code:

import React from 'react'

// Our global theme context with default values
export const ThemeContext = React.createContext({
  themeMode: 'dark',
  setThemeMode: () => {},
})

// Theme provider component with state
const ThemeProvider = props => {
  const [themeMode, setThemeMode] = React.useState('dark')
  const value = { themeMode, setThemeMode }

  return (
    <ThemeContext.Provider value={value}>
      {props.children}
    </ThemeContext.Provider>
  )
}

// Exports a ThemeProvider wrapper
export default ({ element }) => <ThemeProvider>{element}</ThemeProvider>

Set wrapRootElement to your Provider

Within both gatsby-browser.js and gatsby-ssr.js, use Gatsby’s wrapRootElement hook with your new Provider component:

import ThemeProvider from 'layout/ThemeProvider'
export const wrapRootElement = ThemeProvider

Setting and Consuming the global Context

Once you’ve done the above, you’ll need a way to set/consume the Context from within child components within your application.

Since we wrapped the root element of our Gatsby application with the ThemeProvider component, we can get/set the Context state from any component within our app using using React’s useContext hook.

As an example, I have a simple Menu component with a toggle which changes the context state within an onChange event handler:

import { ThemeContext } from './ThemeProvider'

// Dropdown menu component
const Menu = () => {
  const { themeMode, setThemeMode } = useContext(ThemeContext);

  return(
    <Dropdown as={ButtonGroup} toggle alignRight>
      {/* DARK MODE TOGGLE */}
      <ToggleButton
        type='checkbox'
        variant={(themeMode === 'light') ? 'dark' : 'light'}
        checked={(themeMode === 'light') ? true : false}
        value='1'
        onChange={(e) => setThemeMode((e.currentTarget.checked) ? 'light' : 'dark')}
        >&#9788;
        </ToggleButton>

...

Note that in the above example, the output of the useContext hook is destructured, pulling out the global themeMode value, as well as it’s setting function setThemeMode.

Since our Provider is at the top of the application’s component tree, any changes to Context are passed on to all child components and will stay persistent until you refresh the page (eg, like switching the theme of your website).

Note that other data that you need to be persistent across page refreshes (eg, user login/profile data) will probably require a bit more complexity such as local or session storage within the browser.

Hope you found this useful.

Cheers!

Take the Red Pill

you stay in wonderland

And I show you, how deep the rabbit hole goes.

OK!
We'll never share your email with anyone else.
Liked this post? Subscribe! I've heard my newsletter is pretty cool.
OK!