Photo by Christopher Gower on Unsplash

How to use Context, useReducer and LocalStorage in Next JS

Access state from anywhere within your app and keep the data on reload

Thomas Brandt
Published in
5 min readAug 17, 2021

--

Having a global state object accessible from every place of your application comes in rather handy in any type of project. Being able to use a reducer and keep everything in LocalStorage is even more useful.

To do that we’ll be looking at the React useContext and useReducer Hook.

The first thing you are going to want to do is to create a Next JS project with the very goods documentation over at https://nextjs.org/docs .

We’ll start by looking at the Context, then the Reducer and finally at the LocalStorage.

1. Create the Context

Once you’ve created a basic Next js app you will have to create a “context” folder and add an “AppContext.js” file to it. the context is the part that will allow you to share a state with the entire app and the “AppContext.js” file will hold the context and a wrapper to share the data.

my_app/
├─ context/
│ ├─ AppContext.js

First, import all the necessary Hooks from React.

import { createContext, useContext, useMemo } from "react";

Then comes the AppContext variable to create the Context and the AppWrapper which will pass down the context to the rest of the app and which will be exported from this file.

import { createContext, useContext, useMemo } from "react";
const AppContext = createContext();
export function AppWrapper({ children }) { const [appState, setAppState] = useState({}); const contextValue = useMemo(() => { return [appState, setAppState]; }, [appState, setAppState]);
return (
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);}export function useAppContext() { return useContext(AppContext);}
context/AppContext.js

Within the AppWrapper we define an AppState state variable and use the useState hook to initialise it (this is where your state data goes and you can then use the setAppState method to modify it). With the useMemo hook we then memoize the state value for better performance. Then we return a provider that we can use to output the rest of the App with the context value. Finally, we export the useAppContext function which we will then be able to import from everywhere in the App to get the state data.

Now, navigate to the “pages” and “_app.js”, there we will import our AppWrapper and wrap our Layout or App with it.

my_app/
├─ context/
│ ├─ AppContext.js
├─ pages/
│ ├─ _app.js

import "@/styles/globals.scss";
import Layout from "@/components/Layout";
import { AppWrapper } from "@/context/AppContext";function MyApp({Component, pageProps}) {
return (
<>
<AppWrapper>
<Layout>
<Component {...pageProps} />
</Layout>
</AppWrapper>
</>
);
}
export default MyApp;
pages/_app.js

Once you have done this you will be able to use the context anywhere in your app with the useAppContext hook.

import { useAppContext } from "@/context/AppContext";const [appState, setAppState] = useAppContext();
anywhere/anycomponent

Now that you are done with the context, we will be able to add the useReducer hook to improve the state usability.

2. Add the useReducer hook

Create a file called “AppReducer.js” next to “AppContext.js”, it will contain an initialState variable and an AppReducer arrow function. The initialState variable will be used to store the initial state of the app and the arrow function will be used to store the dispatch methods used to modify the state.

export const initialState = {
number: 0,
};
export const AppReducer = (state, action) => {
switch (action.type){
case "add_number": {
return {
...state,
number: action.value + state.number,
};
}
}
};
context/AppReducer.js

In this example, the initialState stores a “number” of value 0 and the AppReducer has a dispatch method called “add_number” which can be called and which increases the number in the state by any value dispatched with the method.

Now that we have the initialState and the reducer we can import them in the context and replace the useState with a useReducer using these two as arguments. (don’t forget to also import useReducer from React to be able to use it)

import { createContext, useContext, useMemo, useReducer } from "react";
import { AppReducer, initialState } from "./AppReducer";

const
AppContext = createContext();
export function AppWrapper({ children }) { const { state, dispatch } = useReducer(AppReducer, initialState); const contextValue = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);

return
(
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
context/AppContext.js

You have now added the useReducer hook to your App context, to use it the procedure is similar to how we used the useState variable and method in the App. Just import the useAppContext hook and instead of the AppState and setAppState destructure the state and dispatch.

import { useAppContext } from "@/context/AppContext";const { state, dispatch } = useAppContext();
anywhere/anycomponent

And this is how you access the data and method (for more information on the useReducer hook visit https://reactjs.org/docs/hooks-reference.html#usereducer .

const { number } = state;dispatch({type: "add_number", value: 3)};console.log(number);
anywhere/anycomponent

Now that you have access to your data and can modify it from everywhere in your App, let’s add a way for it to stay after reload.

3. Keep your data in LocalStorage

Being able to keep the state after reloading the page is very useful, for example, if you want to create a food ordering app and you want to keep your basket after visiting another page.

I will be using the useEffect hook, another solution would have been to create a custom useLocalStorage hook, unfortunately using a hook within a hook is usually more complex than it is useful.

The first step will be to import the useEffect hook from React inside of the AppContext.js file.

import {useEffect, useReducer, createContext, useContext, useMemo } from "react";
context/AppContext.js

Now let’s create two useEffect hooks, one to check if there is already a state inside the local storage (and update the state if there is) and another to update the localstorage with the latest data. for more information on the useEffect hook https://reactjs.org/docs/hooks-effect.html .

useEffect(() => {   if (JSON.parse(localStorage.getItem("state"))) { 

//checking if there already is a state in localstorage
//if yes, update the current state with the stored one
dispatch({
type: "init_stored",
value: JSON.parse(localStorage.getItem("state")),
});
}}, []);useEffect(() => { if (state !== initialState) {

localStorage.setItem("state", JSON.stringify(state));

//create and/or set a new localstorage variable called "state"
}}, [state]);
context/AppContext.js

In the first hook, I use the dispatch method to call “init_stored” from the reducer which looks like this and which replaces the entire state with the stored state.

export const initialState = {
number: 0,
};
export const AppReducer = (state, action) => {
switch (action.type){
case "init_stored": {
return action.value,
}
case "add_number": {
return {
...state,
number: action.value + state.number,
};
}
}
};
context/AppReducer.js

Done! You can now use and update a shared state everywhere in your app and it will not be lost on reload :)

If there are any errors/incoherences or if you want me to write about another topic let me know!

As this is my first publication any kind of feedback is welcome!

--

--

Thomas Brandt

Freelance web developper and management student sharing some of his code and experience.