Global state with React

Introduction

Most of us are very used to the global state concept and we have been applying this pattern in our apps extensively. There are plenty of third party libraries aiming to provide a single place to store our state, but when we are talking about React, Redux is the king in that regard. I bet most of you have been in touch with Redux for quite a long time.

With the release of context API in React 16.3 and especially hooks in React 16.8, a new world of possibilites suddenly arose. As we discovered the cleanliness and simplicity of hooks, Redux started to feel like overly complex and verbose with all those containers, reducers, action creators and so on. The question didn't take too long; if new React context was designed to feed a whole component tree with data, wouldn't it be possible to redefine the global state pattern by using native context plus hooks?

In this post, we are going to show you the main issues of this pattern and a few different approaches to achieve global state using React native features.

In this article we assume that you are already familiar with React Hooks, we will make use of useReducer, useCallback, useMemo... amongst others. You can learn more about it in the official hooks api reference

TL;DR;

React context suffers from a common problem: useContext hook will re-render whenever your context is modified. In other words, every component subscribed to our global state will re-render upon a context change, which in some scenarios might lead to performance issues.

In this post we will go through several techniques to mitigate this problem:

  • Splitting context: if storing everything in a monolith context is problematic, we can split context separating unrelated data, and what's more, we can keep state as close to where it's needed as possible demo.
  • Using containers: we can wrap our components by containers that filter out undesired global state updates. This solution mimicks what was done by Redux containers: listening to global state changes, but only passing down the desired part of the state through props. Although containers will re-render on every context update, they are very light and cheap components to minimize performance issues. demo.
  • Using react-tracked library: Based on the previous solutions, the correct usage of React context seems to imply breaking it into smaller pieces to avoid performance issues. But, for whatever reason, what if we want to have a complex object in context serving as a global state? We can use react-tracked library, that comes with useTrackedState, which uses a Proxy underneath. It tracks state usage in render so that if only the part of state consumed by the component is changed, it will re-render demo.

If you want to read more details about how this works, keep on reading :).

Context as global state

Certainly, context was a high-value addition to React. As they stated:

Context provides a way to pass data through the component tree without having to pass props down manually at every level, more info about it in this link

This is great! We can make data accessible to many different components regardless its level in the tree, that is to say, we can subscribe components to a "global" data store. Just wrap your app within a context provider and feed that provider with the data you want to make global:

const globalState = {
  text: "foo",
};

const globalStateContext = React.createContext(globalState);

const App = () => (
  <globalStateContext.Provider value={globalState}>
    <...>
  </globalStateContext.Provider>
);

Consumption of this data is a piece of cake thanks to the hooks: just retrieve your state by using useContext hook wherever you need it:

const TextComponent = () => {
  const { text } = React.useContext(globalStateContext);
  return <p>{text}</p>;
};

The proposal

In the previous example we just did a basic approach: consuming a static global state. We can't modify text in any way. However, we could go a bit further and build a more flexible solution as most of your scenarios will require to not only consume global data but also modify it.

For that purpose, let's bring useReducer into the equation. It will provide us with a single and constant dispatch method to trigger global state updates. Then, let's make dispatch available to our components through a separate context; sort of having 2 "channels", one to consume global data and another one to modify it.

const defaultGlobalState = {
  num: 0,
  text: "foo",
  bool: false
};
const globalStateContext = React.createContext(defaultGlobalState);
const dispatchStateContext = React.createContext(undefined);

const GlobalStateProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(
    (state, newValue) => ({ ...state, ...newValue }),
    defaultGlobalState
  );
  return (
    <globalStateContext.Provider value={state}>
      <dispatchStateContext.Provider value={dispatch}>
        {children}
      </dispatchStateContext.Provider>
    </globalStateContext.Provider>
  );
};

const App = () => (
  <GlobalStateProvider>
    <...>
  </GlobalStateProvider>
);

For the sake of usefulness and completeness, we have added a few more properties in our global state: num, text and bool.

In order to consume this global state, we opted for creating a custom hook that exposes both state and dispatch in a single call:

const useGlobalState = () => [
  React.useContext(GlobalStateContext),
  React.useContext(DispatchStateContext)
];

So, accessing global state from a component can be as easy as this:

const Counter = () => {
  const [state, dispatch] = useGlobalState();
  return (
    <button onClick={() => dispatch({ num: state.num + 1 })}>
      {state.num}
    </button>
  );
};

Of course, it is important to note that this is not the only valid implementation to accomplish global state within React and you may have your own.

The issue

Even if you just consider the first basic sample with static global state, or our proposal in the previous section, or any other solution of your own based on React context, all of them will suffer from a common problem: useContext hook will re-render whenever your context is modified. In other words, every component subscribed to our global state will re-render upon a context change.

To better understand the consequences of this issue, now think about the previous Counter component: it only uses num property from global state, so it only needs to be informed whenever num is changed. However, Counter will re-render upon bool or text changes, even if num remains the same, but Counter doesn't care about bool or text properties!

So, in summary, all our components "connected" to the global state will re-render under any minimal change in that state, even if it does not affect them directly. That's not how Redux worked, right? This may lead to performance issues. Actually, this is the exact reason why Redux dropped their experimental approach of using direct React context.

To better illustrate this issue, we have built a demo for you:

You can check source code here.

I know what you are thinking: it would be desirable to let useContext subscribe only to a part of the context value (like a selector) to avoid over re-rendering. Before you blame the React team, I will tell you that this topic has an open discussion in the React community. So far, useContext has been working as designed and the problem is closer to the pattern itself: we are trying to keep unrelated properties under a single context.

Approaches

Split context

Well, Dan Abramov himself got involved in this discussion and offered some tips. If storing everything in a monolith context is problematic, let's split context separating unrelated data.

You can inspect source code here.

This idea of multiple contexts is gaining traction in the community. Ken C Dodds goes a step further and suggests not only splitting context but also keeping state as close to where it's needed as possible. Even though this solution moves away from having a single global context, it is a very interesting approach to manage state natively in React.

Containers

Again, inspired by options 2 and 3 by Abramov, we can wrap our components by containers that filter out undesired global state updates. This solution mimicks what was done by Redux containers: listening to global state changes but only passing down the desired part of the state through props. Although containers will re-render on every context update, they are very light and cheap components to minimize performance issues.

Take a look at the code here.

React-tracked

Based on the previous solutions, the correct usage of React context seems to imply breaking it into smaller pieces to avoid performance issues. But, for whatever reason, what if we want to have a complex object in context serving as a global state?

The following approach is built on top of Daishi Kato's react-tracked, a library which is only 1.5kB and has superb performance, aimed to deal with complex objects in context without the hassle of unnecessary re-renders. Now, we could re-write our Counter example like this:

const Counter = () => {
  const [state, dispatch] = useTracked();
  return (
    <button onClick={() => dispatch({ num: state.num + 1 })}>
      {state.num}
    </button>
  );
};

And Counter would only re-render whenever state.num is updated, ignoring the rest of the properties in our global state. Yes I know, this wouldn't be a 100% native solution, but it provides performance and ease of use at a minimum cost, so it's worth checking it out.

Source code is located here.

Resources

We have compiled the demos source code from this post in the following Github repo: global-state-react

Application State Management with React: https://kentcdodds.com/blog/application-state-management-with-react

Github facebook/react discussion, Provide more ways to bail out inside Hooks

React tracked library: https://github.com/dai-shi/react-tracked

Wrapping up

In this article, we have explored several approaches to manage global state using native React context, dropping complex Redux implementations and embracing the cleanliness of hooks.

We have demonstrated the issue with useContext, whose subscription-like mechanism triggers a re-render on every state update, regardless of whether that update is significant for our component or not. Finally, we have presented a few implementations to overcome this problem by either splitting context, blocking undesired re-renders with containers or relaying in a third party solution.

About Basefactor

We are a team of Front End Developers. If you need coaching or consultancy services, don't hesitate to contact us.

Doers/

Location/

C/ Pintor Martínez Cubells 5 Málaga (Spain)

General enquiries/

info@lemoncode.net

+34 693 84 24 54

Copyright 2018 Basefactor. All Rights Reserved.