State management with Next.js and React
The global state can be helpful when components share some common parts. Also, some parts can stay persistent (in local storage) and be used in the following user's session. React provides a native way to handle state management using context with the hooks.
Usage
// ...import { useAppContext } from "context";import { UPDATE_FEATURE_ACTIVATION } from "context/constants";export function CustomComponent() {const { state, dispatch } = useAppContext();// get value from the storeconsole.log(state.isFeatureActivated);// dispatch action to change the statedispatch({ type: UPDATE_FEATURE_ACTIVATION, payload: { isFeatureActivated: true } });// ...}
Context setup
// context/index.jsximport PropTypes from "prop-types";import React, {createContext,useContext,useEffect,useMemo,useReducer,} from "react";import { getItem, setItem, STATE_KEY } from "utils/local-storage";import { INITIALIZE_STORE } from "./constants";import { appReducer, initialState } from "./reducer";const appContext = createContext(initialState);export function AppWrapper({ children }) {const [state, dispatch] = useReducer(appReducer, initialState);const contextValue = useMemo(() => {return { state, dispatch };}, [state, dispatch]);useEffect(() => {const stateItem = getItem(STATE_KEY);if (!stateItem) return;const parsedState = JSON.parse(stateItem);const updatedState = {...initialState,// persistent stateisFeatureActivated: parsedState.isFeatureActivated,};dispatch({type: INITIALIZE_STORE,payload: updatedState,});}, []);useEffect(() => {if (state !== initialState) {setItem(STATE_KEY, JSON.stringify(state));}}, [state]);return (<appContext.Provider value={contextValue}>{children}</appContext.Provider>);}AppWrapper.propTypes = {children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,};export function useAppContext() {return useContext(appContext);}
Reducer with actions
// context/reducer.jsimport { INITIALIZE_STORE, UPDATE_FEATURE_ACTIVATION } from "./constants";export const initialState = {isFeatureActivated: false,};export const appReducer = (state, action) => {switch (action.type) {case INITIALIZE_STORE: {return action.payload;}case UPDATE_FEATURE_ACTIVATION: {return {...state,isFeatureActivated: action.payload.isFeatureActivated,};}default:return state;}};
Wrapper around the app
// app/layout.jsximport { AppContextProvider } from "context";export default function RootLayout({ children }) {return (<html lang="en"><head>// ...</head><body><AppContextProvider>{children}</AppContextProvider></body></html>);}
Constants
// context/constants.jsexport const INITIALIZE_STORE = "INITIALIZE_STORE";export const UPDATE_FEATURE_ACTIVATION = "UPDATE_FEATURE_ACTIVATION";
Boilerplate
Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.