State management with Next.js and React

zsevic

Željko Šević

Posted on January 14, 2023

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 store
  console.log(state.isFeatureActivated);
  // dispatch action to change the state
  dispatch({ type: UPDATE_FEATURE_ACTIVATION, payload: { isFeatureActivated: true } });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Context setup

// context/index.jsx
import 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 state
      isFeatureActivated: 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);
}
Enter fullscreen mode Exit fullscreen mode

Reducer with actions

// context/reducer.js
import { 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;
  }
};
Enter fullscreen mode Exit fullscreen mode

Wrapper around the app

// app/layout.jsx
import { AppContextProvider } from "context";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
      // ...
      </head>
      <body>
        <AppContextProvider>{children}</AppContextProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Constants

// context/constants.js
export const INITIALIZE_STORE = "INITIALIZE_STORE";
export const UPDATE_FEATURE_ACTIVATION = "UPDATE_FEATURE_ACTIVATION";
Enter fullscreen mode Exit fullscreen mode

Boilerplate

Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.

💖 💪 🙅 🚩
zsevic
Željko Šević

Posted on January 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related