A comparison of global state management in React

May-26-2024

Comparison of state management libraries with React

Recently I spent more time with React, trying to go a bit deeper than what I used to look at. I tried to look at two different global state managements libraries: Jotai, Zustand and at React Context; This post will be the first in a series of three posts dedicated to this topic.

The Problem

Let’s imagine that you have data at the core of your application, and one component deep in the component tree needs to access and/or modify this data. By default, React shares data from the top to the bottom through props, so at its most basic form you would need to give this data to props to each component in the tree. That is called Prop Drilling and it makes your code verbose and less elegant.React Context can be a solution included into the React basic library. Some other third-parties libraries are provided solutions which are more elegant and/or with better performance.

The Application

As example of an application, we will code a Kanban type application. We will display a set of tasks in different columns, where one column represents a certain state (To Do, In Progress, Done). We will have the following components:

  • A container which will be the overall board
  • A column container which will display all tasks of a certain state
  • A card corresponding to one Task, which contains buttons to change task status.

We can see that there should be a global state, shared by all components - and the task card should be able to modify this global state. (If this is not clear, try to think about how you would have one task “moving” columns if each column was keeping track only of “its” tasks).

Here is the wireframe we are looking into: Kanban wireframe

Implementation with React Context

React Context is part of the React API, and is a way for a component to share some data with all of its children component. The typical examples are UI theming (dark / light themes) or auth-related values (e.g. current authenticated userID etc.). Note that the data shared can be of any type, which includes functions. So we can actually share a state variable and a modifier function in the case we need to allow children components to modify the state.

Note: Each time the context changes, all the children components will be re-rendered, even the ones which don’t use the actual context. This is the main drawback of React Context.

How does it work

In order to implement React Context, you need three steps:

  1. Declaring the context (and its type) and exporting it for use in upmost component. (It doesn’t have to be the root, but it has to be quite high).
  2. Wrapping the children of this component into a <Context.Provider> element which will have as property the value passed down the component tree.
  3. In the component which needs to access the shared global value, you need to import and use the useContext hook to access the value.

In the case the consumers just need a read-only value, you can provide only the value. In our case we need to provide the value and some modifier functions

Code in our example

Here are the different pieces of code. Because provider and exporter need to import the context object, we define it in a separate file called TaskContext.tsx.

Here is the definition

// Type of value provided by the context
type TaskContextType = {
  taskList: Task[];
  setTaskList: (taskList: Task[]) => void;
  upgradeTask: (taskName: string) => void;
  downgradeTask: (taskName: string) => void;
};

// Note that the object passed is not the initial or default value, but just a placeholder value
export const TaskContext = createContext<TaskContextType>({
  taskList: [],
  setTaskList: () => {},
  upgradeTask: () => {},
  downgradeTask: () => {},
});

The provider will then have the following code

// at the top of the file
import { TaskContext } from './TaskContext.tsx';

// Then inside the component body

return (
    <TaskContext.Provider value={providedValue}>
      { children }
    </TaskContext.Provider>
  );

And then inside the component(s) where we want to access the data:

// At the top of the file
import { useContext } from 'react';
import { TaskContext } from './TaskContext.tsx';

// ....

// Inside the component
  const context = useContext(TaskContext);

  return (
    <div className="taskBox">
      { /*  ... */ }
      <div className="buttonBar">
        <button onClick={() => context.downgradeTask(task.name)}>-1</button>
        <button onClick={() => context.upgradeTask(task.name)}>+1</button>
      </div>
      { /*  ... */ }
    </div>
  );

The code is available at Stackblitz

In a next spot we will have a look at the implementation with Zustand