Skip to main content
Back to blog

Software development

Handling global state in React in 2022

Jean-Christophe Séguin Cabana
Apr 22, 2022 ∙ 15 mins
coding in React

I was recently asked by a friend, who’s learning React, if it was still worth it learning Redux, given that the native Context API also provides some solution to handle global state and share data between components. It’s a fair question that often comes up. I’ll address later how the latter is in fact not really ideal for the state portion. It’s always a matter of the right tool for the right job. In some cases, Redux would be like eating cereal with a shovel, but in others, it would offer a great solution to handle a few requirements at once: state, client-side caching, debugging, etc. I ended up sharing with my friend what would probably be my favorite solution these days (depending on the nature of the application of course). His question made me want to dig a little deeper into the subject and see what is in the landscape these days.

In this blog post, I’m going to define and break down a bit what we mean by “global state” in the context of React. I’m then going to explore some of the popular solutions that are available to us in 2022 and ask questions in the likes of: Is this the right library for my use case? Is it overkill? Does it overlap with another installed library I’m not fully leveraging? Does it synergize well? Etc. I will be more interested in exploring the reflections that these modern solutions initiate, rather than showcasing a big list of tools and listing their pros and cons. My goal is to ask myself the right questions in the future, when it’s time to weigh the options for a global state solution, and get to know new or revamped popular libraries. Hopefully it will help you achieve the same.

Anatomy of a global state in React

One way we can see global state in the context of React is essentially persisted, centralized and globally accessible information that determines how some components that care about it will be rendered. It represents, as stated by the first principle of Redux, your app’s source of truth. It differs from local state, which is meant to encapsulate the data flow within a component. React has pretty straight-forward solutions to handle local state, but is un-opinionated about how data should globally be handled, hence the numerous available solutions and frequent debates.

For the sake of this post, I wanted to dissect the global state a bit more, to better understand our exact requirements. After reading on a couple of variations of breakdown, I ended up with these two simple categories:

  • Client state: any sort of global client feature. It could be, for example, a complex UI flow where you want to persist information from one step to another, or something simple like dark/light mode color schemes, or complex entity instances affecting different components that we may want to update.
  • Server state: asynchronous data that is fetched, cached, synchronized and updated from the server state via an API (REST or GraphQL) or WebSockets.

The reason why I like this simple breakdown is that, as pointed out by the React-Query team, server state is quite different from client state:

For starters, server state:

  • Is persisted remotely in a location you do not control or own
  • Requires asynchronous APIs for fetching and updating
  • Implies shared ownership and can be changed by other people without your knowledge
  • Can potentially become "out of date" in your applications if you're not careful

Server state data has different challenges (caching, pagination, lazy loading, knowing when data is out of date, etc). React developers traditionally often chose Redux to handle most of these cases and put everything into a single store. But, in recent years, we’ve seen more specialized and adapted solutions come up like React-Query, SWR or RTK Query (developed by Redux Toolkit, for those attached to Redux) to specifically tackle server state. Some of these solutions can even make a global state library unnecessary, if your application doesn’t have global client features. We’ve also seen simpler minimalistic options that could be better suited to only handle client state, like Zustand or Jotai.

That brings our first big question: does my application have some complex client-side states, does it predominantly deal with server state or does it require both? We’ll use the premise that we are dealing with a medium to large sized application that necessitates both for this blog post (similarly to most projects we deal with at Osedea).

Stick with Redux?

When Dan Abramov, the co-author of Redux, was recently asked the question “when should I use Redux?” during a casual interview, I was a bit surprised, yet glad to hear what he had to say. Hesitating a bit initially, his first answer implied that if it’s already used by the team, it’s a good enough reason by itself. It’s a valid point that, especially in a larger team, having standards can offer some benefits (easier onboarding, tested and proven conventions, reusable implementations, etc.). But one can wonder if the standard in place is still relevant and that’s kind of what Dan expresses about Redux afterwards.

When asked what he would use today if he started a new project, he answered that he probably wouldn’t even use Redux, but instead use a more specialized library to handle server state and caching (which is often what developers use Redux for), like React-Query, Apollo or Relay. I want to reiterate that it depends on the use case of course, as Dan also points out. As for states that are more local or client-side only, he said he would hoist it to the top of the application, with something like Context for instance. It’s interesting to observe that we once again have that client/server state breakdown we saw earlier.

So why Redux then? Well now that Redux Toolkit offers RTK Query, which is specialized in server state, we can argue that we have a solution that suits all of our needs. If you are used to Redux Toolkit, that you are dealing with a REST API (we’ll see later how an application using GraphQL would benefit from a better combo) and want to take full advantage of all the features, then Redux is a very valid choice. But there is another important principle in programming that Redux is not famous for: KISS (Keep It Simple Stupid). Redux Toolkit offers abstractions/tools meant to simplify the implementation of Redux, but even then, there is still a lot more boilerplate when we compare with more recent libraries.

Let’s compare Redux Toolkit with a newer solution

By now you must be telling yourself: “nice blog post, but where are the code snippets bro?”, so let’s get to that. I will use Zustand for this exercise, since it has simplicity written all-over. Zustand indeed claims to be a “small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated”. We’ll use the basic implementation example from the Zustand documentation, but let’s start with Redux Toolkit:

import React from "react";
import ReactDOM from "react-dom";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";

const initialState = {
  bears: 0,
};

const bearsSlice = createSlice({
  name: "bears",
  initialState,
  reducers: {
    increment: (state) => {
      state.bears += 1;
    },
    removeAll: (state) => {
      state.bears = 0;
    },
  },
});

const { increment, removeAll } = bearsSlice.actions;

const bearsReducer = bearsSlice.reducer;

const store = configureStore({
  reducer: {
    bears: bearsReducer,
  },
});

function BearCounter() {
  const bears = useSelector((state) => state.bears.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  const dispatch = useDispatch();
  const increasePopulation = () => dispatch(increment());
  return <button onClick={increasePopulation}>one up</button>;
}

function App() {
  return (
    <>
      <BearCounter />
      <Controls />
    </>
  );
}

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Now let’s see what Zustand has to offer:

import React from "react";
import ReactDOM from "react-dom";
import create from "zustand";

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

function BearCounter() {
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

function App() {
  return (
    <>
      <BearCounter />
      <Controls />
    </>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

We can notice how Zustand stripped down the boilerplate to the bare minimum and achieved the same result in half the lines of code. In a normal application, this example would of course be split into different files to make it organized and scalable, but that gives us an idea.

Additionally, Zustand offers very simple implementations of common requirements. For example, selectors memoization:

const users = useStore(useCallback(state => state.users[id], [id]))

Or persistence:

import create from "zustand"
import { persist } from "zustand/middleware"

export const useStore = create(persist(
  (set, get) => ({
    bears: 0,
    increasePopulation: () => set({ bears: get().bears + 1 })
  }),
  {
    name: "bears-storage", // defaults to localStorage
  }
))

Zustand can use Redux devtools. It has a very simple way of dealing with async actions. You can easily set up middlewares, use it outside of React, and much more. So to conclude, it seems to check most of the boxes that Redux provides, but with a simpler and more intuitive implementation.

Nice, so we have a satisfying combo, using a server state specialized library, let’s say React-Query, plus a simple client state library like Zustand. But why not simply use Context for client state, as suggested by Dan Abramov, and avoid the hassle of installing and updating another library?

What about Context?

I don’t want to get into too much detail on that topic, because it has already been covered in some great and more in-depth articles, but to sum it up, we can go back to the definition from the React team: “Context provides a way to pass data through the component tree without having to pass props down manually at every level.” It was never meant to, as Mark Erikson puts it, manage anything: “Context is a form of dependency injection. It is a transport mechanism - it doesn't "manage" anything. Any "state management" is done by you and your own code, typically via useState/useReducer”

This is why, in a good implementation, you will normally see a couple of Context(s), and not just one big central Context hoisted at the top of the application. This means that Context defeats the principle of “single source of truth” that we saw earlier. Why not do it? Because, as the React team mentions in the “Caveats” section: “Context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders”. Performance is good enough of an argument for me. But also, the fact that Context doesn’t meet the criteria of what makes a state management tool, as explained by Mark Erikson.

Context could still be a good complement to a server state library, if the client-side information we want to pass down is minimal, scoped by application sections and doesn’t need to be centralized. This way, we make sure that we render components only on changes.

Any other interesting combos?

Given that we are still talking about a medium to large sized application that requires both client and server states, we’ve seen how there seems to be a tendency to split those into two different sets of tools. How would we choose these two libraries then? Since server state data is often what “takes the most place” in the global state, I would start by checking what sort of server API we are dealing with.

With GraphQL?

Even though in reality GraphQL simply requires one POST endpoint, developers typically use a GraphQL client-side library, to have a clean implementation of queries and mutations. Among the most popular ones, we have Apollo Client, Relay and URQL. The three of them offer some caching solutions. This cache can be manually updated and is accessible throughout your application. We can argue that these caching solutions are not all necessarily simple to use (talking to you Apollo Client!), but they do cover the server state portion in that fashion.

We can always decide to only use the query/mutation abstractions, ditch their caching solution and use, for instance, Redux to store and cache the data, but that would be missing out on something we already have in the box, when we install such a GraphQL client. That being said, I can also easily imagine a team losing control and using a mixture of data centralized/selected from the Apollo Client cache, while some other pieces of fetched data are stored into Redux, which can create a lot of confusion and make us lose our centralized source of truth…

But if you decide to take that route, any combination is valid: Relay + Zustand, URQL + Jotai, Apollo Client + Redux Toolkit even. The main take-away here is to ask ourselves the right questions. How can these options best synergize? How do we avoid overlapping? How do we make it clear in our application that server and client states are stored differently? And finally, is the library I’m choosing for client state overkill, in a scenario where I only need the centralized information portion?

With REST?

REST APIs still being the popular standard, they adapt really well with any of the general global state solutions we’ve mentioned so far. The advantage of using a specialized library for server state though, is to benefit from abstractions that make it easier to deal with the API cache we store on the client side. The three popular choices I often hear these days would be RTK Query, React-Query and SWR. Just remember that RTK Query is an addon to Redux Toolkit and not a stand-alone solution.

Before we conclude…

I obviously didn’t mention all the popular solutions that are offered to us these days. I will list the ones we’ve seen so far, plus a couple more options, in case you wish to explore further.

For general global state

To use in complement with a server state solution, if required, or implement your own within its ecosystem:

  • Jotai - a “primitive and flexible state management for React” that uses atoms as pieces of states.
  • MobX - a “battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP)”.
  • Recoil - a fastly growing state management library for React, developed by the React team.
  • Redux Toolkit - “intended to be the standard way to write Redux logic”
  • XState - A “JavaScript and TypeScript finite state machines and statecharts for the modern web.”. Different approach than the others with different features, but nonetheless interesting.
  • Zustand - “A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated”

And for server state

  • Apollo Client - “a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL”
  • React-Query - “Performant and powerful data synchronization for React”
  • Relay - “a JavaScript framework for fetching and managing GraphQL data in React applications that emphasizes maintainability, type safety and runtime performance”
  • RTK Query - “optional addon included in the Redux Toolkit package”, “eliminating the need to hand-write data fetching & caching logic yourself”
  • SWR - “a React Hooks library for data fetching”
  • URQL - “a highly customizable and versatile GraphQL client for React, Svelte, Vue, or plain JavaScript”

Notice how some of these are for React only and others are more agnostic…

It’s a bit overwhelming… I’m still not sure how to choose

We’ve discussed in detail about some of the main criteria when it’s time to pick the right state libraries. Here are some of the important questions we’ve seen so far:

  1. Do I need both client and server states?
  2. What are the exact features my application requires for global state?
  3. If I mostly need server state, is my client-side information simple and scoped enough to use some Context(s) to pass it down, or do I need a more centralized and persisted solution (aka a state).
  4. Do I need a GraphQL specific solution and does it already cover my server state needs?
  5. Are my picks for server and client states in good synergy? Are they optimal?
  6. Are these libraries easy to implement and maintain?

We can go a bit further by adding an excellent list of ten quality attributes proposed in an article on the subject of React state management:

  1. Usability
  2. Maintainability
  3. Performance
  4. Testability
  5. Scalability (works with the same performance on the bigger states)
  6. Modifiability
  7. Reusability
  8. Ecosystem (has a variety of helper tools to extend the functionality)
  9. Community (has a lot of users and their questions are answered on the web)
  10. Portability (can be used with libraries/frameworks other than React)

This way you can complete your own research and come up with the best solution for your application’s requirements.

Can I choose… no library?

A colleague of mine at Osedea, Zack, recently went in a rather different direction and eliminated the need of global state libraries altogether. He’ll be sharing his approach in an upcoming Osedea Blog post, where he proposes a ground-breaking solution using Domain Driven Design principles and Object Oriented Programming, that even almost completely decouples global state from React, apart from a few simple adapters made of React hooks.

It originated from a specific use case around various entities, containing heavy business logic, that needed to be shared between different services, in an agnostic way. In short, there is never a “one size fits all” solution and you should always take the time to think about your application’s requirements, before you pick a solution.

So what did you say to your friend?

Back to my friend’s initial question, which was “is it still worth it to learn Redux…”? I answered that I think it is worth it, yes, especially for a developer in learning. It’s still often the standard in the industry. Once he masters this, he can learn the simpler recent solutions. Now what would I personally use today, if I started a new project? It doesn’t matter. None of it matters. We’re all 1s and 0s cruising at high velocity in a doomed galaxy.

Sources:

Photo credit: Lautaro Andreani.