DEV Community

Cover image for A comprehensive overview of React State libraries
Petr Janik
Petr Janik

Posted on • Updated on

A comprehensive overview of React State libraries

Background

I have started learning React two months ago. At that time, I was overwhelmed by all the different possibilities of React. I have addressed two of them already in the previous aticles of this series. The remaining one - state management - will be discussed in this article.
Throughout my learning, these examples have proven priceless to me as I always had some React sandbox to play and experiment with. They served as a reminder of what I already learnt. I hope they will be of use to you as well.

How this works

I have implemented a simple TODO app in every state management way I came across. The functionality is pretty basic. You can list existing todos and add a new todo.
It uses a backend running on heroku. https://todo-backend-rest.onrender.com/todos/ for REST API and https://todo-backend-graphql.onrender.com/graphql for GraphQL API. (The service goes to sleep when idle, so please allow some time after the initial request for the service to spin up.)
The code is on codesandbox.io so you can easily run it or fork it and edit.

Table Of Contents

State in class components

We create a class component and use its state property and setState method.

useState hook

Earlier, when we wanted to manage the state, we had to use a class component. This is no longer the case with the arrival of hooks. This sandbox uses useState hook to manage the state.

useThunkReducer

This sandbox uses useThunkReducer from react-hook-thunk-reducer, which is similar to the built-in useReducer, but unlike useReducer, it allows us to dispatch async actions that are needed for fetching. This is an alternative to Redux and redux-thunk.

Redux with redux-thunk

This approach has proven to be the most verbose one.
Writing Redux with TypeScript results in even more boilerplate code.
Redux needs another helper library for side effects (fetching etc.) such as redux-thunk or redux-saga.
In this example redux-thunk is used.
This sandbox contains:

  • an older approach using mapStateToProps and mapDispatchToProps connected with react-redux connect HOC function
  • a newer approach using useSelector and useDispatch hooks

Redux with redux-saga

Redux again, but this time using redux-saga instead of redux-thunk.

MobX class component

Mobx is used for state management (both local and global) and for observing.
This sandbox contains:

  • an older approach using class stores and @inject and @observer annotations.
  • class component using inject and observer HOC
  • functional component using inject and observer HOC The store is provided via Provider component from mobx-react.
<Provider {...store}>
  <TodoList/>
</Provider>
Enter fullscreen mode Exit fullscreen mode

This approach is deprecated and the following ones taking advantage of React Context should be used.

MobX and context (not null)

Here we take advantage of custom useStores hook.

const {TodoStore} = useStores();
Enter fullscreen mode Exit fullscreen mode

The useStores hook consumes storesContext via useContext hook.
storesContext is initialized to { TodoStore: new TodoStore() } so we do not need to provide the context in <storesContext.Provider> component.

MobX and context (null)

If we didn't want to create context with initial value as in previous approach, we could create a custom <StoreProvider> component. This component returns a <storesContext.Provider>.
The useStores hook now also checks, whether the store (i. e. the value of context) is not null.
This sandbox also contains 4 ways of observing the state:

  • observer HOC with regular function
  • observer HOC with arrow function
  • <Observer> component
  • useObserver hook

MobX and useLocalStore

We have seen useLocalStore hook used in the MobX and context (null).
From the MobX documentation:

The naming useLocalStore was chosen to indicate that store is created locally in the component. However, that doesn't mean you cannot pass such store around the component tree. In fact it's totally possible to tackle global state management with useLocalStore despite the naming. You can for example setup bunch of local stores, assemble them in one root object and pass it around the app with a help of the React Context.

Which is exacty what we did in the previous example.
In this example, however, we insert the code of the store directly into the component.

Functions like observer can be imported from mobx-react-lite, which is a lighter version of mobx-react. It supports only functional components and as such makes the library slightly faster and smaller. Note, however, that it is possible to use <Observer> inside the render of class components. Unlike mobx-react, it doesn't support Provider/inject, as useContext can be used instead.

React plain Context

We can create a global state in App component and then pass it to other components by using React Context.
Modern solution using useContext hook.

Older solution using Context.Consumer render props component.

Apollo Client

Here, we use Apollo's useQuery and useMutation hooks.
Previously, we had to use apollo-link-state to manage state with Apollo. As of Apollo Client 2.5, local state handling is baked into the core, which means it is no longer necessary to use apollo-link-state.

React Query

useQuery and useMutation hooks with caching, optimistic updates and automatic refetching.
This and many more features are available with React Query.
React Query works with Promise-based APIs.
The following sandbox demonstrates use with both REST API (fetch) and GraphQL API (graphql-request – a Promise-based GraphQL client).

XState

Uses finite states machine to manage state.
XState repository.

Vercel's SWR

SWR works with Promise-based APIs.
The following sandbox demonstrates use with both REST API (fetch) and GraphQL API (graphql-request – a Promise-based GraphQL client).
SWR repository.

Zustand

As their README says:

Small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, that isn't boilerplatey or opinionated, but still just enough to be explicit and flux-like.

Zustand repository.

Easy Peasy

A redux-like library. Uses store, StoreProvider, dispatching of actions and thunks etc. It is compatible with Redux DevTools.
Easy Peasy repository

React Recoil


Getting started

MobX-state-tree

Getting started

MobX-state-tree with flow function

flow function is a suggested way to handle asynchronous actions. There are multiple advantages to it, including direct modification of its own instance. Also the onAction middleware will only record starting asynchronous flows, but not any async steps that are taken during the flow.
Read more about flow in documentation.
The difference is in model's fetchTodos action.

RxJS

In this example I used a common global store with RxJS Subject to which individual components can subscribe their setState function. Changes are dispatched by calling functions on the store.

This article about RxJS with React Hooks for state management explains this concept really nicely.

Redux Toolkit

A less-boilerplatey Redux. Personally, I have really enjoyed this one. It is compatible with Redux code you have been using so far.

Same functionality, but taking advantage of Redux Toolkit's createAsyncThunk function:

Read more about Redux Toolkit.

localStorage API

By utilizing localStorage API, we fetch todos from the endpoint only once. Subsequent page refreshes load todos from the localStorage unless the localStorage entry is deleted. The downside is that if someone else changes the todos in the database behind the endpoint, we'll still see the old data.

Read more about localStorage API.

IndexedDB API

This approach is functionally similar to the localStorage API. We fetch todos from the endpoint only once. Subsequent page refreshes load todos from the IndexedDB unless the database is deleted. Again, the downside is that if someone else changes the todos in the database behind the endpoint, we'll still see the old data.
Unlike localStorage, which is useful for storing smaller amounts of data, IndexedDB is a low-level API for client-side storage of significant amounts of structured data.

Read more about IndexedDB API.

URL

In this example we use useUrlState hook to store the state in the URL. When there are no URL parameters, the todos are fetched from the endpoint and then reflected in the URL. The URL parameters are the source of truth – sharing the URL shows the same todos to whoever opens the link.

Immmer

Whereas useState hook assumes any state that is stored inside it is treated as immutable – we write setState(newState) – Immer allows us to write setState(produce(oldState => { mutate oldState })).
We can further simplify this by using useImmer hook from the use-immer package instead of the useState hook which allows us to omit the produce function: setState(oldState => { mutate oldState }).
This sandbox contains both approaches.

So which one should you choose?

First thing to note is that those ways are not mutually exclusive, you can make use of both Redux and Apollo Client at the same time.
I'd say that Redux is a lot of fun and provides a nice way of debugging when using redux-devtools-extension. However, the code overhead is huge, especially when combined with TypeScript. For smaller projects, I would choose MobX-state-tree instead or even plain React Context with hooks for smaller applications.

This article (from 2016) discusses the advantages and drawbacks of Redux.

Resources:

Mobx docs
React Redux docs
React docs
Cover photo by v2osk on Unsplash.

Top comments (2)

Collapse
 
jackedwardlyons profile image
Jack Lyons

Great post! You should think about adding Overmind to the list :)
overmindjs.org/views/react

Collapse
 
petr7555 profile image
Petr Janik

Thank you for the suggestion. I will definitely add that one!