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.
React.createContext
React.useState
gatsby-ssr.js
and gatsby-browser.js
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>
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
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')}
>☼
</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!