How To use React Context API with useReducer, useMemo Hooks

How To use React Context API with useReducer, useMemo Hooks

Intro and about this Context API tutorial

Hello, if you are searching for best practices and how to use React Context API and leverage the whole React Hooks with it, this post might help you. Like you, I was also looking at how to use the Context API, how to combine different useContext(s), use it with useReducer, useMemo and it took me many different React Context API examples to figure out a clean structure and scalable way to use useContext (similar to Redux), and to leverage the whole React Hook ecosystem behind it.

Lots of the tutorials about React Context and useContext covers only partial usage: it is a long process to find and connect all the different React Context examples to use the Context API ecosystem at its best; this is why of this tutorial.

Before diving further with React Context with Hooks, you need to be familiar or have some experience with the following React Hooks:

Why and when to use the React Context API?

  • to avoid external dependency of state management if possible
  • to avoid props drilling in the React component tree
  • reduce the bundle size from small to medium sized web apps
  • leverage the React Hook APIs ecosystem

How to setup the React Context API with useReducer

What should be held in React Context? What kind of data is ideal in the global application state? Good examples are:

  • user authentication / authorization
  • theming dark / light mode
  • detecting mobile devices
  • managing global modals, drawers
  • etc…

As you can understand the data should be more static with few or little updates. This translates into this initial elementary state for our React Context:

const GLOBAL_STATE = {
  isLoggedIn: undefined,
  theme: "light",
  isModalOpen: false,
}

💡 Tip

Keep in mind that you want to update that state at the beginning of your application or when there is a global setting change.

Let’s draft the first version of our global state component using createContext and useReducer. Make a new folder named store in the src folder and inside of it, create globalStore.js.

import { useReducer, createContext } from "react";
// Define the initial state
const GLOBAL_STATE = {
  isLoggedIn: undefined,
  theme: "light",
  isModalOpen: false
};
// Define the reducer
const globalReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      return {
        ...state,
        isLoggedIn: true
      };
    case "LOGOUT":
      return {
        ...state,
        isLoggedIn: false
      };
    default:
      return state;
  }
};
// Create the context
export const GlobalState = createContext();
GlobalState.displayName = "GlobalState";
export const GlobalStateProvider = ({ children }) => {
  // Create the reducer
  const [state, dispatch] = useReducer(globalReducer, GLOBAL_STATE);
  const value = {
    ...state,
    login: () => {
      dispatch({ type: "LOGIN" });
    },
    logout: () => {
      dispatch({ type: "LOGOUT" });
    }
  };
  // Wrap the context provider around our component
  return <GlobalState.Provider value={value}>{children}</GlobalState.Provider>;
};

Let’s add a few more action types to the reducer to extend our global state actions:

import { useReducer, createContext } from "react";
// Define the initial state
const GLOBAL_STATE = {
  isLoggedIn: undefined,
  theme: "light",
  isModalOpen: false
};
// Define the reducer
const globalReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      return {
        ...state,
        isLoggedIn: true
      };
    case "LOGOUT":
      return {
        ...state,
        isLoggedIn: false
      };
    case "LIGHT_THEME":
      return {
        ...state,
        theme: "light"
      };
    case "DARK_THEME":
      return {
        ...state,
        theme: "dark"
      };
    case "OPEN_MODAL":
      return {
        ...state,
        isModalOpen: true
      };
    case "CLOSE_MODAL":
      return {
        ...state,
        isModalOpen: false
      };
    default:
      return state;
  }
};
// Create the context
export const GlobalState = createContext();
GlobalState.displayName = "GlobalState";
export const GlobalStateProvider = ({ children }) => {
  // Create the reducer
  const [state, dispatch] = useReducer(globalReducer, GLOBAL_STATE);
  const value = {
    ...state,
    login: () => {
      dispatch({ type: "LOGIN" });
    },
    logout: () => {
      dispatch({ type: "LOGOUT" });
    },
    setLightTheme: () => {
      dispatch({ type: "LIGHT_THEME" });
    },
    setDarkTheme: () => {
      dispatch({ type: "DARK_THEME" });
    },
    openModal: () => {
      dispatch({ type: "OPEN_MODAL" });
    },
    closeModal: () => {
      dispatch({ type: "CLOSE_MODAL" });
    }
  };
  // Wrap the context provider around our component
  return <GlobalState.Provider value={value}>{children}</GlobalState.Provider>;
};

Now we have completed our globalState.js. As you can notice, the code for it is already pretty long, and we will refactor it later 🙂

How to setup useContext in the application

Now that we have our global context, we need to wrap the GlobalStateProvider around the App.js component in the index.js.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { GlobalStateProvider } from "./store/globalState";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    // Wrap the App component inside the GlobalStateProvider
    <GlobalStateProvider>
      <App />
    </GlobalStateProvider>
  </StrictMode>
);

After adding this code to the index.js, we can finally useContext inside App.js or any sub-component. I will only use the isLoggedIn property and login and logout actions in my example.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { GlobalStateProvider } from "./store/globalState";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    // Wrap the App component inside the GlobalStateProvider
    <GlobalStateProvider>
      <App />
    </GlobalStateProvider>
  </StrictMode>
);

And you should get a working setup like:

React Context API with hooks example

Congratulations, our tiny global application state is working as expected. In this example, I want to point out that changing the state inside the component will trigger the re-rendering of the whole application.
Use the global state props as close as possible to the deepest React component; you will avoid unnecessary re-renders of other React components that do not need to re-render.

How to refactor createContext, useContext to have a clean code architecture like Redux

Now, the real fun begins 🙂. In this section, we will refactor the code to make it more manageable and scalable, and finally, the folder structure will look like the Redux one, which means if you need to migrate to Redux one day, it will be straightforward.

Refactoring globalState.js

The refactored files names will combine the state we want to store and the javascript scope that these files contain. In the /src/store folder, make a new one called global, and inside of it, create 4 more files:

  • global.actions.js
  • global.provider.js
  • global.reducer.js
  • global.state.js

global.actions.js

export const globalActionTypes = {
  LOGIN: "LOGIN",
  LOGOUT: "LOGOUT",
  LIGHT_THEME: "LIGHT_THEME",
  DARK_THEME: "DARK_THEME",
  OPEN_MODAL: "OPEN_MODAL",
  CLOSE_MODAL: "CLOSE_MODAL"
};

global.state.js

import { createContext } from "react";

export const GlobalState = createContext();
GlobalState.displayName = "GlobalState";

global.reducer.js

import { globalActionTypes as actions } from "./global.actions";

export const globalReducer = (state, action) => {
  switch (action.type) {
    case actions.LOGIN:
      return {
        ...state,
        isLoggedIn: true
      };
    case actions.LOGOUT:
      return {
        ...state,
        isLoggedIn: false
      };
    case actions.LIGHT_THEME:
      return {
        ...state,
        theme: "light"
      };
    case actions.DARK_THEME:
      return {
        ...state,
        theme: "dark"
      };
    case actions.OPEN_MODAL:
      return {
        ...state,
        isModalOpen: true
      };
    case actions.CLOSE_MODAL:
      return {
        ...state,
        isModalOpen: false
      };
    default:
      return state;
  }
};

global.reducer.js

import { useReducer } from "react";
import { globalReducer } from "./global.reducer";
import { globalActionTypes as actions } from "./global.actions";
import { GlobalState } from "./global.state";

const GLOBAL_STATE = {
  isLoggedIn: undefined,
  theme: "light",
  isModalOpen: false
};

export const GlobalStateProvider = ({ children }) => {
  const [state, dispatch] = useReducer(globalReducer, GLOBAL_STATE);

  const value = {
    ...state,
    login: () => {
      dispatch({ type: actions.LOGIN });
    },
    logout: () => {
      dispatch({ type: actions.LOGOUT });
    },
    setLightTheme: () => {
      dispatch({ type: actions.LIGHT_THEME });
    },
    setDarkTheme: () => {
      dispatch({ type: actions.DARK_THEME });
    },
    openModal: () => {
      dispatch({ type: actions.OPEN_MODAL });
    },
    closeModal: () => {
      dispatch({ type: actions.CLOSE_MODAL });
    }
  };

  // Wrap the context provider around our component
  return <GlobalState.Provider value={value}>{children}</GlobalState.Provider>;
};

Let’s update index.js, import GlobalStateProvider from ./store/global/global.provider replace the relative code and inside of the App.js update the GlobalState import ./store/global/global.state.js and update again the relative code. If you did all correctly the app should work as before.

We sliced the code into smaller javascript files, which improves readability and scalability: when more people are working on the same project.

Main takeaways of the code React Context refactoring

  • code easier to maintain and extend
  • it will help to migrate to Redux if you choose to
  • multiple people can work on it easier as code is split into multiple files

How to combine multiple React Contexts into one component?

The simplest solution is to wrap each context inside the other one. Therefore, this solution leads us to “wrap hell,” which reminds us of our old javascript foe “callback hell”:

<ContextProviderOne>
  <ContextProviderTwo>
    <ContextProviderThree>
      <App />
    </ContextProviderThree>
  </ContextProviderOne>
</ContextProviderOne>

We can do better than that. Redux has an elegant and clean solution for combining multiple reducers called combineReducers – in the search for a similar solution, I encountered this valuable tutorial that explains how to combine Contexts into one and preserve clean code.

For our solution, we will make something very similar, but without the use of TypeScript. We will split the code into 2 files, one will hold the logic to combine all the Contexts into one, and the other component will wrap the combined Contexts around our React children component.

Let’s create our two files in src/store/:

  • ContextProviderComposer
  • CombinedContextProviders

ContextProviderComposer

This component will combine all the contexts and return a single one.

import React from "react";

const ContextProviderComposer = ({ contextProviders, children }) => {
  return contextProviders.reduceRight(
    (children, parent) => React.cloneElement(parent, { children }),
    children
  );
};

export default ContextProviderComposer;

CombinedContextProviders

In this component, we specify all the different Contexts we want to combine. Note that when we pass the providers, we have to add a unique key property for each one.

import ContextProviderComposer from "./ContextProviderComposer";
import { GlobalStateProvider } from "./global/global.provider";

const CombinedContextProviders = ({ children }) => {
  return (
    <ContextProviderComposer
      contextProviders={[<GlobalStateProvider key={"global_state_provider"} />]}
    >
      {children}
    </ContextProviderComposer>
  );
};

export default CombinedContextProviders;

And now update the index.js:

import { createRoot } from "react-dom/client";

import App from "./App";
import CombinedContextProviders from "./store/CombinedContextProviders";
// import { GlobalStateProvider } from "./store/global/global.provider";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  // <GlobalStateProvider>
  <CombinedContextProviders>
    <App />
  </CombinedContextProviders>
  // </GlobalStateProvider>
);

If everything is fine, the login/logout functionality should work as before. Now we can combine different Contexts into one. For example, we can split the global .state (and related files) into multiple ones like:

  • login.state.js,…
  • modal.state.js,…
  • theme.state.js,…

I leave this exercise/practice up to you – If you need help, you can leave a comment.

How to nest and optimize useContext with useMemo

Last but not least, we can add some optimizations and discuss some best practices to avoid re-renders from the root of your application. There are 3 ways:

1. useContext in nested components

The simplest way to optimize the Context is to useContext in nested components inside App.js. When the state changes, only the related component containing the useContext will re-render. Example:

import { useContext } from "react";
import { GlobalState } from "../store/global/global.state";

const DisplayAuth = () => {
  console.log("re-render subcomponent");

  const { isLoggedIn, login, logout } = useContext(GlobalState);

  return (
    <>
      <div className="App">{isLoggedIn ? "Welcome back" : "Please login"}</div>
      <button onClick={login}> Login </button>
      <button onClick={logout}> Logout </button>
    </>
  );
};

export default DisplayAuth;

2. Memoize the Context values with useMemo

The simplest way to optimize the Context is to useContext in nested components inside App.js. When Wrap the Context value inside of useMemo, all values will be memoized, and useMemo will only recompute the memoized value when one of the dependencies has changed.
As an example, open globa.provider.js import useMemo, wrap the variable value inside of it, and add state as a dependency to useMemo:

 const value = useMemo(() => ({
...state,
// rest of the code, check it here
// https://codesandbox.io/s/how-to-use-contex-use-reducer-use-memo-drhumc?file=/src/store/global/global.provider.js
}), [state]);

3. Fine-tune the dependencies of useMemo

There are particular scenarios when you want to save a state, despite not wanting to trigger the UI to re-render, yet. A practical example could be that the user needs to input their data, and you to save it, but There are particular scenarios when you want to save a state, despite not wanting to trigger the UI to re-render yet. A practical example could be that the user needs to input their data, and you need to save it, but the UI needs to be updated when the user submits the form. You can do so by including or excluding the dependencies in the useMemo.

// pseudo code
 const value = useMemo(() => ({
...state,
//...
}), [state.property_1, state.property_2"]);

If you are forced to use something like that, try first to refactor the code and split the Contexts state into different ones, as this technique is very advanced, if you do not really understand what are you doing, your application won’t re-render and might look broken.

How to debug React Context API

You can debug React Context in 3 ways:

  • For more straightforward projects, the good old friend console.log() will help you out, but you get lost pretty quickly with more complex codebases.
  • Use paint flashing from the browser rendering menu (Chrome / Edge) to visually see which parts are re-rendering.
react context How To use React Context API with useReducer, useMemo Hooks
  • One of the best tools that Redux has for debugging is the ReduxDevTools. React Context, unfortunately, does not have such a feature-rich tool, but there is something similar React Context DevTool. As you can see, the name of the Context is GlobalStatewhich was set in by the code in the previous examples.
React Context API development tools

Conclusion and resources

As you learnt with React Context you can really stop using Redux (or other state management libraries) for smaller up to mediums sized project or apps. However depends also on the scalability that you need from the project, what is the team size and the frequency of data updates you need to handle – this is a topic for another tutorial.

This was my first tutorial blog: I am following tutorials an blog for more than a decade so I genuinely hope you learned something new, valuable and enjoyed by doing so. Your feedback and support are more than welcome.

You can find the working tutorial react application here.

Don’t forget to leave comments and likes, as it will give me support for future posts.

2 Comments

  1. Denedo Oghenetega

    This is a nice article and it’s really structured in a way that’s easy to understand. There are two things I like here, first one is how you were able to refactor the context file into separate files that are easier to maintain and scale and secondly the helper function for combining multiple context providers to avoid wrapping so many context providers on top of each other is nice.

    1. Andrea Scotto Di Minico

      Thank you so much for your feedback. Glad to hear the tutorial was valuable to you. I’ll try to keep it up with more React tutorials.

Leave A Comment

Your email address will not be published. Required fields are marked *