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
- Class components
- useState
- useThunkReducer
- Redux with redux-thunk
- Redux with redux-saga
- MobX class component
- MobX and context (not null)
- MobX and context (null)
- MobX and useLocalStore
- React plain Context
- Apollo Client
- React Query
- XState
- SWR
- Zustand
- React Recoil
- MobX-state-tree
- MobX-state-tree with flow function
- RxJS
- Redux Toolkit
- localStorage API
- IndexedDB API
- URL
- Immer
- React Automata TBD
- Unstated TBD
- Unstated Next TBD
- Relay TBD
- React Async TBD
- Overmind TBD
- Akita TBD
- Hookstate TBD
- Jotai TBD
- Valtio TBD
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
andmapDispatchToProps
connected withreact-redux
connect
HOC function - a newer approach using
useSelector
anduseDispatch
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 frommobx-react
.
<Provider {...store}>
<TodoList/>
</Provider>
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();
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.
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
MobX-state-tree
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 (3)
Great post! You should think about adding Overmind to the list :)
overmindjs.org/views/react
Thank you for the suggestion. I will definitely add that one!
Can add my library as well github.com/asmyshlyaev177/state-in...