Skip to main content

React State Management Libraries: Which one?

Have you ever used a badly written website and annoyingly had to type in everything again, or re-select all the choices one by one? These are problems of state management.

Every front-end developer will be tempted to use a state management library at some point. Here at Matillion, there have been quite a few discussions on which is the best. In reality, there is no single right answer. Everyone has different opinions and preferences, and every project has different needs.

At Matillion we have a Hack Day every month, which is a day to research, collaborate and solve problems outside our daily work commitments. I found this a good opportunity to gather some pros and cons of different state management libraries. My aim was to simplify this decision process.

 

What is UI State?

UI state means what the user sees at any moment in time. The state changes as the user interacts with the application by supplying data and navigating around.

Well-managed state helps create a more fluid, coherent, productive and pleasant user experience.

There are two types of state: UI State and Server Caching State. For this research I focused on UI State. It is an easy, managed way of sharing states and data between components relying very little on React’s Context or not at all.

 

What is a State Management Library?

A State Management Library is a pre-built code bundle that can be added to a front end such as React. Its purpose is to help manage an application’s state.

Not all React projects need a state management library. Some are just fine with React.Context. However, I found with slightly more complex projects React.Context can be difficult to deal with, as it heavily relies on React’s tree. Also, there is no way to keep track of dynamic components and their contexts. Overall it can be quite frustrating to use. 

So I decided to look at the most common or talked about libraries at the time of writing (mid 2022) which are Recoil, Jotai, Redux and Zustand. Many more libraries are available but the basic outcomes from these four can be applied to all the others.

 

Atomic vs Global Store

I looked into two types of State Management Libraries: Atomic and Global Store. You can imagine an Atomic Library as a 3rd dimension to the React Tree which is perpendicular to the existing nodes. With the help of selectors, these atoms can be directly accessed through individual nodes without the need to go up a React node Tree which makes it a lot faster. They use methods similar to React hooks. Alternatively, the Global Stores I looked into follow a FLUX approach; Action → Dispatcher → Store → View. These libraries have completely independent store management file(s) in the project which work together with the rest of the application to provide the best state management solution.

Atomic State Libraries – Recoil, Jotai

  • Save the state within the React Tree
  • Follow a more similar approach to useState

Global Store State Libraries – Redux, Zustand

  • Save the state outside the React tree

 

Recoil – Pros and Cons

This is an atomic library from Facebook. This means it has the highest chance to be compatible with future React versions, as well as being maintained.

Recoil uses atoms and selectors.

 

Recoil – Pros

  • Compatible with the latest React features
  • URL query sharing of states – derived data and queries
    • Share a URL using a state id in the URL
    • Interesting, but I had quite a hard time implementing (see cons below)
  • Flexible shared state 
  • App-wide state observation
  • Easy to integrate
    • All you need to do is add <RecoilRoot/> to the main app then use useRecoilState(someNewState) instead of useState to share state between components
    • Extract the state to another file and import it from there to be used. 
    • The someNewState is now an atom that needs to be defined. 
  • Works well with an infinite number of atoms 
    • You can easily make any number of atoms using a memorize function to only generate it once rather than repeatedly
  • Can be easily implemented for any level or data structures
  • You can make the state derived 
  • Pure functions
  • Caches
  • Can pass links with state changes for powerful DOM manipulations

 

Recoil – Cons

  • Experimental, so comes with some risk
  • Suitable for simple solutions
  • Tied to React
  • Not very good at garbage collection. The dependency on string keys can be ineffective and can lead to memory leaks
  • Larger bundle size than Jotai – 48KB minified 
  • Poor documentation – I tried deep-linking the global app state to allow sharing a single link of the app which includes the entire state, but I wasn’t able to due to the lack of documentation
  • Requires manual clean up
  • Uses local storage for state persistence

 

Jotai – Pros and Cons

This is another open source Atomic library

 

Jotai – Pros

  • Minimalistic API
  • Not experimental 
  • Better TypeScript support than Recoil 
  • Good documentation 
  • Based on Javascript’s WeakMap to track atoms (whereas Recoil is based on unique string keys). This means the Javascript engine automatically handles garbage collection, leading to better memory handling and performance
  • Small bundle size of only 8.5KB minified 

 

Jotai – Cons

  • No direct way of sharing the entire app’s state (all atoms at once without constructing an individual atom object). Can use useAtomsSnapshot at the risk of memory leaks

 

Redux – Pros and Cons

Redux is a Global Store, Based on a Flux architecture model. The main Flux characteristic is a single source of truth (the store) which can be updated by dispatching actions.

It’s a global store, using actions, reducers, dispatchers and stores.

 

Redux – Pros

  • Very flexible. Ideal if you need a more sophisticated solution
  • The structure of the files tends to be the same across projects, which can make it easier for other developers to understand the code
  • Can be used with server side rendering. Although if using other libraries such as useQuery this is redundant. 
  • Local Storage for state persistence
  • Used by many many developers therefore a lot of resources for problems and solutions
  • Can be used well with other Redux libraries like Redux-Saga and Redux-Form. However recently the React community has shifted towards using more direct and specific libraries for different problems instead of one big one that can solve everything.

 

Redux – Cons

  • Very flexible! This can lead to a lot of different implementation, stale stores, redundancy etc.
  • Many setup steps required
  • A very large library 
  • The average React app will only need a fraction of Redux functionality

 

Zustand – Pros and Cons

Zustand is a Global Store, and seems to be an in-between of Atomic and Flux solutions. Overall it is a relatively simple library, which can be both an advantage and a disadvantage.

 

Zustand – Pros

  • Interacts nicely with outside of React as the stores can live outside of the components or functional components – it is not tight in React’s context, which can help with micro-frontends
  • Quicker and more concise than writing Redux, with no need for such a lot of setup
  • Can easily handle persistence using persist. Keep in mind this uses localStorage by default, which comes with its own risks
  • Gives a flexible, Redux-like approach for people that prefer writing in Redux
  • Jotai and Zustand were made by the same creators – If you have a complex app you could potentially have both in your application for different use cases 
  • Easy async calls within stores

 

Zustand – Cons

  • The documentation is not great 
  • The TypeScript implementation is not straightforward 
  • A single store at a time, which can end up consuming a lot of space

 

Testing Atomic libraries

Testing atomic libraries can be tricky as they use a hook approach. The main difficulty is mocking or initialising the state specifically for some of the atoms, instead of indirectly testing the state by manipulating the DOM and asserting against it (e.g. update text input to check the input state atom).

Jotai recommends using the react-hook-testing-library, specifically the renderHook method. However, this library will become deprecated. As of testing-library/react version 13.1.0 onwards renderHook is included alongside everything else necessary (e.g. act) which depends on React version 18 and above. React-hook-testing-library only works with React version 17 and below. If you’re using React version 18 onwards, please use @testing-library/react version 13 and above which has adopted a lot of the hooks from react-hook-testing-library.

Here is a worked example

// Initialise the yourAtomHook that's used in the component with some different value
const { result } = renderHook(() => useAtom(yourAtomHook))

act(() => {
  result.current[1]({ id: 3, text: "some-fake-text" })
})

const { getByRole } = render(<Card id={3} />)
const inputEl = getByRole("textbox") as HTMLInputElement

expect(inputEl.value).toContain("some-fake-text")

 

Testing Global Store libraries

Global state mocking/initialising is more conventional. You can either set the state using your real store or by mocking it. For the Zustand Library you can follow their documentation on how to reset the store on each test.

Worked example for Zustand:

// Initialise the usePickerStore's state pickerColor with a different color

usePickerStore.setState({ pickerColor: "red" }, true)

const { container } = render(<Message />)

expect(container.textContent).toContain("red")

 

Real Examples – CodeSandbox

To help you experiment with these libraries, I have created some demos in codesandbox. 

I have not created a demo for Redux, as it is the most widely used and has a lot of resources already.

 

Which State Management Library should you choose?

As you can see, each library has its own pros and cons, and each use case will differ.

At Matillion, among our many front-end projects we mainly use Redux and React.Context. The reasoning behind this choice was:

  1. Redux is most familiar to many of the developers
  2. Some projects are really small, so we did not want to introduce additional dependencies
  3. Some projects were created at a time when Atomic libraries were not the most conventional and reliable way of sharing state.

However, we have since found that our Redux project is not actually using all of Redux’s potential.

Many developers at Matillion like the promises Atomic libraries give, and some even use them for their personal projects. So in the future, for new Matillion projects we are planning to use Jotai as the default library to deal with global states.

I have put together a diagram that will hopefully help visualise the differences between each library. Please use it as a reference, taking into account the pros and cons of each option.

 

Written by Evdokia Mina, Software Engineer.