Competing State Management Tools in React Applications

State management is one of the most complex parts of frontend web development, in my opinion. Partially because of how it sneaks up on you. Most applications start small, and so using React’s useState is fine. In cases when you wish to share stateful data between sibling components, you could use the useContext hook. However, I learnt early to structure my applications is a way that let me pass states through drilling. All my state, for most applications, were drilled as props. Even useContext was used sparingly. Mostly because I considered the overheard of Redux as too overwhelming.

It was a few months after I started learning web development and delved into programming for the first time. I could barely wrap my head around React. And then I found myself learning Redux, for state management. It appeared in many of the tutorials I took for web development, advertised as a basic necessity. Begrudgingly, I managed to implement it one time, but I can’t say I understood it. I had a store, reducers, actions, and the ancillary dispatches and switch statements, and they all worked as they should have. However, the entire experience left me knowing I wanted to avoid Redux for as long as I could. So there I went, drilling states (as props) through several components to use them where I needed it. Until recently, when I was confronted with a large enough project that I had to manage state on an application wide level. And then I was opening those Redux tutorials on YouTube again.

If I had to explain to someone how most state management tools in React work, I would tell them to think of three major parts: a store, reducers, and actions. Many tools, such as the Redux Toolkit and Zustand, now abstract over the process. So it takes considerably less code to set up a store and you can include both reducers and actions while setting up your store. In this way, reducers and actions are sort of merged (in the store).

The way it used to work was that an action dispatched a type and payload. And the data got passed to the reducers. Reducers would have switch statements, which looked over the types and determined the piece of code that should be run for different kinds of types. Based on a type, the reducer could change the state (immutably, because state changes in React are usually immutable). This involved at least a few files and much boilerplate. Once you figure out the pattern, it could be a while before you get to any real though over your application. Because the boilerplate is a lot. This is why Redux gets such bad press. But outside of that, it also increases the barrier of entry for learning Redux, because there’s a lot of boilerplate code that often gets glossed over.

Zustand was a solution I stumbled upon, shortly after I had to implement Redux. It was significantly easier to use - I had a working counter app in a few minutes from when I came across it. Although I’m now a much better engineer than when I first tried to learn Redux, I could still clearly see how much lower the barrier of entry for Zustand was. There was significantly less boilerplate code. I’m particularly glad I do not have to pass every function to alter my application’s state into dispatch with it. Also, most of the code is in the store. Unlike with Redux, I do not have to wrap a Provider component with the store passed as an argument to the top-most level of my React App. Instead, the store looks something like this:

import create from "zustand";

const useStore = create((set) => ({
     counter: 0,
     incrementCounter: () =>
          set((state) => ({ counter: state.counter + 1 })),
}));

export default useStore;

That is all the code I needed to initialize my store, stored in a store.js file. And I could import the store wherever I needed it and use either counter, the value stored in the store, or incrementCounter, the function to alter the value of the state. Accessing the value would look like this:

import useStore from "./store";

const counter = useStore((state) => state.counter)

And with that little piece of code, you would have access to the counter value in your Zustand state. You can alter the value through:

const incrementCounter = useStore((state) => state.incrementCounter)

You can then pass the incrementFunction as a callback function to your event (or call it wherever you want). The library even lets you automate the creation of selectors, so you do not have to call useStore over and over again. Remember, the repetition is why we boo Redux. Suffice to say that I like the project enough that for the first time, I am attempting to make substantial contributions to an open source project.

Someone mentioned it while I was gruelling away at switch statements and actions, but I did not realize how much better it improved the Redux user experience. Not until I made a tweet, pointing out how much boilerplate was in Redux, and one of the Toolkit maintainers pointed out the solution. However, I find that I still prefer Zustand. The Redux Toolkit still needs functions to alter state wrapped in dispatch, and the Provider component still needs to wrap the main react App. Other options I also intend to explore in the future are Jotai (for mutable state changes) and React Query.