In this article, I will show how you can use React Context with React Hooks to store global state across a React app, then store that state in local storage. This can be used for example to store light vs dark theme, then whenever the user visits your website again they will have the same theme they last selected. Which leads to an improved experience.
Structure
Note: We will be using typescript
We will use a project structure like so:
.
├── src
│ ├── App.tsx
│ ├── index.html
│ ├── index.tsx
│ ├── providers
│ └── views
├── LICENSE
├── package.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
Note: This application was based on saltyshiomix’s template
Getting Started
Our package.json
file looks like this:
{
"name": "ExampleApp",
"version": "1.0.0",
"scripts": {
"start": "serve dist"
},
"dependencies": {
"react": "16.9.0",
"react-dom": "16.9.0"
},
"devdependencies": {
"typescript": "3.6.2"
}
}
The example application linked will also be using babel for transpiling our code to Javascript
and Webpack for bundling our code into a single index.js
file.
App
Now onto how we can use React Hooks to persist user settings in local storage. So every time they visit our website it will “restore” their previous setting, such as theme, light or dark.
DarkModeProvider.tsx
React Contexts can be used to store the global state of our application. Such as our current theme, this can then be accessed anywhere in our application and also changed anywhere. React contexts provide us with two “sub-components”, a provider and, a consumer for that specific React context.
- Provider: The component that will provide the value of the context (stored)
- Consumer: The component that will consume the value
Context provides a way to pass data through the component tree without having to pass props down manually at every level. - https://reactjs.org/docs/context.html
React hooks allow us to access the React context from within functional components. In our case, it means we don’t have
to use the React context’s consumer we can use React hooks instead to use the context, this can be seen in the MainApp.tsx
First, let’s create our React context that will store the current theme the user has selected. It will also give us a function that other components can use to update the theme. Finally, after any change has been made it will update the local storage with the users latest settings.
import React, { Context, createContext, useReducer, useEffect } from "react";
export const LIGHT_THEME: Theme = {
background: "#fafafa" as BackgroundColors,
color: "#000000" as ForegroundColors,
isDark: false
};
export const DARK_THEME: Theme = {
background: "#333333" as BackgroundColors,
color: "#fafafa" as ForegroundColors,
isDark: true
};
export type BackgroundColors = "#333333" | "#fafafa";
export type ForegroundColors = "#000000" | "#fafafa";
export interface Theme {
background: BackgroundColors;
color: ForegroundColors;
isDark: boolean;
}
interface DarkModeContext {
mode: Theme;
dispatch: React.Dispatch<any>;
}
const darkModeReducer = (_: any, isDark: boolean) =>
isDark ? DARK_THEME : LIGHT_THEME;
const DarkModeContext: Context<DarkModeContext> = createContext(
{} as DarkModeContext
);
const initialState =
JSON.parse(localStorage.getItem("DarkMode") as string) || LIGHT_THEME;
const DarkModeProvider: React.FC = ({ children }) => {
const [mode, dispatch] = useReducer(darkModeReducer, initialState);
useEffect(() => {
localStorage.setItem("DarkMode", JSON.stringify(mode));
}, [mode]);
return (
<DarkModeContext.Provider
value={{
mode,
dispatch
}}
>
{children}
</DarkModeContext.Provider>
);
};
export { DarkModeProvider, DarkModeContext };
Next, we will import all of the modules we will need to use then. We will define our two different themes LIGHT_THEME
and DARK_THEME
. Then finally because we are using Typescript we will define types for the Themes and the context we
will use.
const darkModeReducer = (_: any, isDark: boolean) =>
isDark ? DARK_THEME : LIGHT_THEME;
Next, we will define a reducer. A reducer is a pure function which does not use the state of the
current app so it cannot have any unintended side-effects. Exactly the same functions we
would define if we were using Redux. In this case, the reducer just returns the DARK_THEME
if the isDark
argument is true
else it returns the LIGHT_THEME
.
const DarkModeContext: Context<DarkModeContext> = createContext(
{} as DarkModeContext
);
const initialState =
JSON.parse(localStorage.getItem("DarkMode") as string) || LIGHT_THEME;
After this, we create our React context called DarkModeContext
and we give it a default empty object
(we don’t really mind too much). We then define the default value. It tries to check the value
stored in localstorage
. If there is none, then we use the LIGHT_THEME
. After which we define the provider.
const DarkModeProvider: React.FC = ({ children }) => {
const [mode, dispatch] = useReducer(darkModeReducer, initialState);
useEffect(() => {
localStorage.setItem("DarkMode", JSON.stringify(mode));
}, [mode]);
return (
<DarkModeContext.Provider
value={{
mode,
dispatch,
}}
>
{children}
</DarkModeContext.Provider>
);
};
export { DarkModeProvider, DarkModeContext };
The provider is what is used to give other components access to the context. Here you can see
we use the useReducer
hook and give it our darkModeReducer
with the initial value. This
reducer will then return a mode
which is the current theme data and a function dispatch
which will be used to update the current theme. Breaking it down a bit further we see:
useEffect(() => {
localStorage.setItem("DarkMode", JSON.stringify(mode));
}, [mode]);
Next, we define the useEffect
hook which is called every time the mode
is changed, by the
dispatch
function being called. Hence the we have the [mode]
at the end. It very simply
stores the current theme into the user’s local storage under the key DarkMode
. Now if
this was changed from light -> dark and then the user comes back to the site, the initial value
we would get from localstorage.getItem("DarkMode")
would not, of course, be the dark theme.
return (
<DarkModeContext.Provider
value={{
mode,
dispatch,
}}
>
{children}
</DarkModeContext.Provider>
);
//...
export { DarkModeProvider, DarkModeContext };
Finally, we create the Provider component we will export, the mode
is the theme data that other
components can use and dispatch
is the function other components can use to change the current
theme. As long as they are a child of the DarkModeProvider
hence the {children}
which will be a prop.
App
Our “Main” app page we will import the Provider that will export from our providers folder. This means any component that is a child of this will be able to access and update the current theme, we will see how to do that later on.
Warning: The provider needs to be in a separate component to those that access the React Hook. Hence we import the
MainApp
component rather than including all of theMainApp.tsx
inApp.tsx
.
import React from "react";
import { DarkModeProvider } from "~/providers/DarkModeProvider";
import MainApp from "~/views/MainApp";
const App = () => {
return (
<DarkModeProvider>
<MainApp />
</DarkModeProvider>
);
};
export default App;
Note: The module resolver allows us to refer to src/ folder as ~ in our imports. I wrote a whole article about how you can use it here (#ShamelessPlug)
Now the MainApp is a very basic page: it contains a single button which is used to toggle our theme for dark to light and vice versa. Here we use React hooks with React context to be able to update and retrieve the theme.
import React, { useContext } from "react";
import { DarkModeContext } from "~/providers/DarkModeProvider";
const MainApp = () => {
const theme = useContext(DarkModeContext);
const { background, color, isDark } = theme.mode;
return (
<div
style={{
background: background,
color: color,
minHeight: "100vh"
}}
>
<div>Theme is {isDark ? "Dark" : "Light"}</div>
<button onClick={() => setTheme(theme)}>Change Theme</button>
</div>
);
};
const setTheme = (darkMode: DarkModeContext) => {
const isDark = darkMode.mode.isDark;
darkMode.dispatch(!isDark);
};
export default MainApp;
useContext
The useContext
is an example of a React Hook. It allows users to access a specific context from with a functional
component, a component which is not a class. The context has a mode property which stores the current theme we should
display light or dark. Such as background
and color
.
const theme = useContext(DarkModeContext);
const { background, color, isDark } = theme.mode;
This is then used in our “CSS” styling to style the page background and button colour. We also show the current theme that is set on the page.
Change Theme
So we can access the data from our React context but how do we change the theme? Well, we use the button, which
has an onClick
event. The setTheme
function gets the current theme from the isDark
property of the context.
It then calls the dispatch
function we have defined in the context to change to the theme to the opposite
it is at the moment. So light theme -> dark theme and dark theme -> light theme.
<button onClick={() => setTheme(theme)}>Change Theme</button>;
//...
const setTheme = (darkMode: DarkModeContext) => {
const isDark = darkMode.mode.isDark;
darkMode.dispatch(!isDark);
};
That’s it! We successfully created a very simple React app that leverage React hooks and React context to allow us to store the user’s settings into local storage so it can persist and the user will be able to use the same settings they set last time, such as dark mode instead of the light mode.
Appendix
- Example source code
- Photo by Cristian Palmer on Unsplash