What is the best state container library for React?
Spoiler: I generally use none. And you might not need one either.
First, I’d like to underline: I’m not saying that any of the below listed libraries are useless or that you shouldn’t be using them. Proper tooling choice depends on business needs. Each library I mention in this article is a masterpiece of inventive engineering, each can make huge positive impact on DX and would brilliantly fit in some use cases.
It’s just that in 5 years of using React I’ve rarely got to work on an application that would desperately require, or at least slightly benefit from using global state containers. On the other hand, I’ve seen quite a few apps in which state containers were used as some sort of crypts, where human laziness, anti-patterns and loads of spaghetti were buried.
How so? I’ll explain. But let’s start with a reflection on why we are using state containers in the first place.
Why state containers?
Generally state containers serve 2 purposes, which are:
- Store global state of your application (i.e. state that is not related to 1 particular component in the app, but rather to the app as a whole);
- Deliver changes to components. I.e. you want any component in the app to be able to access global state, and dependent components to be updated when some part of global state changes.
Many state containers provide additional tooling, like undo/redo stack, and integrate with special developer tools. Often they are also opinionated on how you should organise state updates. For example, the Redux concept would break if you mutated the state object, thus Redux in development mode would emit warnings. It is important to understand that these are just neat additional features, not the core purpose of state containers.
Let’s look at how different libraries approach this core purpose, and what additional features they come up with.
Options that we have
This list is definitely not exhaustive. These are state container solutions that I used, and I like how each of them brings up some new interesting concepts, making the list look like some sort of evolutionary chain. If you know a good state container lib that you love and I didn’t mention it, please tell us in the comments section :)
Raw (immutable) JavaScript object
We are now going to do some serious magic. We are going to write our own state container library. Get ready for real hardcore engineering. Or, wait a minute…
const stateContainerContext = React.createContext(null);export const StateProvider = ({ initialContainerState, children }) => {
const [containerState, setContainerState] = useState(initialContainerState) return (
<Context.Provider value={{ containerState, setContainerState }}>
{children}
</Context.Provider>
);
}export const StateConsumer = ({ children }) => {
const contextValue = useContext(stateContainerContext);
if (contextValue === null) throw new Error('StateConsumer can only be used as (sub)child of StateProvider')
const { containerState, setContainerState } = contextValue; return children(containerState, setContainerState)
}
Voila, a state container! Less than 20 lines of code. I’m using function components with hooks here, but equivalent components could also be created using “regular” React class components with state. If you are not familiar with hooks, I highly recommend you to read about them, hooks is probably the best thing that ever happened to React.
Back to business, that’s how we use our state container:
export const Counter = () => {
<StateContainerConsumer>
({containerState, setContainerState}) => (
<div>
<span>{containerState.count}</span>
<button
onClick={() =>
setContainerState({count: containerState.count + 1})
}
>
Increment
</button>
</div>
)
</StateContainerConsumer>
}export const App = () => {
return (
<StateContainerProvider initialContainerState={{count: 0}}>
<Counter />
</StateContainerProvider>
)
}
In this example I’m using render prop technique, but if we replaced it with higher order component we would get something very similar to Redux. Basically it is rather a semantical than an architectural difference.
Redux
The heart of Redux is nothing more than what we did above. The rest of ~7kb minified is utility functions to operate on complex state objects + some development time warnings. What Redux really adds from architectural point of view is opinion on how you should update state. It introduces concepts of actions (read: objects of shape {type: string: payload: object}
), reducers (read: function that takes previous state and action and returns next state) and middlewares (read: a convenient way to share business logic between reducers).
If our app were more complex than the hackneyed counter example, these concepts would appear very useful. But again: there’s no magic. Redux is 99% the idea and 1% the code. You might need to be Dan Abramov to initially come up with the idea, but you only need to know some basic JS and React to implement it.
I find the idea behind Redux beautiful because it is simple yet powerful. The main reason why people don’t like Redux is because it makes you write a lot. You have to use lots of semantics to express very simple changes. Let’s add another counter to our example, and say now we have 3 buttons — one increments counter A, another one increments counter B, and the 3rd one increments both. First, we need 3 action types:
export const INCREMENT_COUNTER_A = 'INCREMENT_COUNTER_A';
export const INCREMENT_COUNTER_B = 'INCREMENT_COUNTER_B';
export const INCREMENT_BOTH_COUNTERS = 'INCREMENT_BOTH_COUNTERS';
Redux apps that I have seen usually had 1 large file that included all action types. On the one hand, this makes sense — your linter would notice any possible naming collision and warn you. On the other hand, this goes against separation of concerns — unrelated features from all parts of the app stuck together in one single place.
We also need to make our reducer capable of handling these action types.
const reducer = (prevState, action) => {
switch (action.type) {
case INCREMENT_COUNTER_A:
return {...prevState, counterA: prevState.counterA + 1}
case INCREMENT_COUNTER_B:
return {...prevState, counterB: prevState.counterB + 1}
case INCREMENT_BOTH_COUNTERS:
return {
...prevState,
counterA: prevState.counterA + 1,
counterB: prevState.counterB + 1
}
default:
return prevState;
}
}
We are having to do a lot “by hands”, and the amount of manual work is proportional to the complexity of the app. These are pretty simple operations, looks like a good place for automation. Currently we need immutability to make components aware of the changes, but how nice it would be if we could do something like container.counterA++
…
Vanilla MobX
MobX solves the immutability problem by leveraging the concept of observable. With MobX, we can mutate store object directly, the library will track changes and automatically update dependent components:
export const container = observable({
counterA: 0,
counterB: 0,
someOtherProp: 'foo'
})export const Counter = observer((container) => {
return (
<div>
<span>{container.counterA}</span>
<span>{container.counterB}</span>
<button
onClick={action(() => container.counterA++)}
>
Increment counter A
</button>
<button
onClick={action(() => container.counterB++)}
>
Increment counter B
</button>
<button
onClick={action(() => {
container.counterA++;
container.counterB++
}}
>
Increment both counters
</button>
</div>
)
})
During first render, MobX will memoize that your component reads values of counterA
and counterB
. It will re-render the component whenever any of these props are changed, but won’t do it if someOtherProp
is mutated, saving you renders.
Notice that we wrap every state change with action
higher order function. This is an optional yet highly recommended practice for 3 reasons:
- It makes MobX aware of the fact that your mutation is intentional, suppressing console warning.
- MobX will batch state changes to prevent wasted renders. If we wouldn’t wrap the function that increments both counters with
action
, two re-renders would happen — first forcounterA
update then forcounterB
. - Oftentimes updater functions don’t just mutate state but also read it. MobX automatically considers every read that it can track a subscription. Wrapping updater fn with
action
prevents you from accidentally subscribing your updater fn to changes and making the component re-render when you don’t expect it to.
Additionally, you can assign each action a name for debugging purposes.
I’m diving into that technical detail here because that’s how I see the spirit MobX — it does a lot of automation for us, at the same time providing us utilities to customise or opt out. Excellent, looks like that’s what we desire —smart automation.
MobX is also associated with object-oriented approach, as opposed to pure functional Redux. It allows declaring getters, setters and methods, thus moving from a raw state object to a rich entity that can be referred to as model. Although OOP is not in a big favour in the modern world (and this is genuinely for a reason), I would call this a benefit, just because I like encapsulation. Keeping some piece of data and functions that work with this piece of data close to each other is definitely beneficial to developer experience.
Now, what’s missing?
- No Redux dev tools. With Redux, we could open console and see the state of the entire app at each particular moment, time travel and so on, which was cool.
- No paradigm. Redux docs tell us exactly how to make it. MobX is more of a generic tool that is usage agnostic. Should we have 1 big store per app or multiple small stores? What are do-s and don’t-s? Everyone understands it in their own way.
- Less predictability. This is direct outcome of having too much freedom. Redux state at any given moment is nothing more than an object. Redux reducer is nothing more than a function that takes state and action and returns state. MobX with its observables and reactions is harder to reason about.
Which brings us to our next candidate.
MobX-state-tree
MobX-state-tree (hereinafter referred to as MST) is my personal favourite and a much underestimated library. It combines best parts of Redux’s immutability (transactionality, traceability and composition) and MobX’s observability (discoverability, co-location and encapsulation).
MST is built on top of MobX and takes advantage of its flexible subscription system, at the same time approaching state management in a more opinionated way.
Store in MST is a “living tree”, that consists of shape (runtime type information) and state (data). That’s what it looks like (I will be using examples from official docs here, just because they are fully sufficient):
const Todo = types
.model("Todo", {
title: types.string,
done: false
}
)
.actions(self => ({
toggle() {
self.done = !self.done
}
}));
const Store = types.model("Store", {
todos: types.array(Todo)
});
const store = Store.create({
todos: [
{
title: "Get coffee"
}
]
});
In conjunction shape and state make it possible to represent data both as a rich, mutable object-oriented model and a raw immutable object (snapshot). Similarly, changes can be represented as a set of mutations (or patches) applied to model, and a set of intermediate immutable snapshots.
We can create and call mutating methods:
store.todos[0].toggle();
And at the same time, at any given point of time get immutable snapshots:
onSnapshot(store, snapshot => {
console.dir(snapshot)
})
MST also ships with development-time validation. Another nice idea of MST is volatile state, i.e. bits of state that are local, non-serialisable and not subject to be located in storage, but that we might need to access from actions. These are such things as timers, promises, pending requests and etc.
const Store = types
.model({
todos: types.array(Todo),
state: types.enumeration("State", ["loading", "loaded", "error"])
})
.actions(self => {
let pendingRequest = null // a Promise
function afterCreate() {
self.state = "loading"
pendingRequest = someXhrLib.createRequest("someEndpoint")
}
function beforeDestroy() {
// abort the request, no longer interested
pendingRequest.abort()
}
return {
afterCreate,
beforeDestroy
}
})
Imagine doing same thing with Redux. If you are a Russian-speaker (unfortunately there’s no translation available) I would recommend you reading or watching this talk on MST by Azat Razetdinov. While comparing Redux to MST he uses an expressive analogy: Redux is like drawing a cartoon by hand, snapshot by a snapshot. MST is like CGI animation: you create a model and programmatically move it the way you like. In both cases you end up with a set of frames, but the amount of human effort differs dramatically.
Of cause this magic has a price. MST is:
- Harder to learn. You need to learn vanilla MobX first, then additionally MST. MST docs are 4 times larger than this article.
- Less popular compared to MobX and much less popular — compared to Redux. Which means smaller community and less tooling. It is harder to convenience your teammates or supervisor to use it. Newcomers to the project have less chances to already know it.
- Has less predictable performance compared to both MobX and Redux.
- Debugging is simpler than with vanilla MobX but (very arguably though) harder than with Redux.
Apollo-link-state
I would say Apollo-link-state is the most special lib in this list. It is not a self-sustained state container solution but a part of Apollo GraphQL stack. If you are not familiar with GraphQL or Apollo it is definitely worth learning, but here is a quick recap. GraphQL is basically this:
You have a GraphQL server and you can query various shapes or data from it, depending on what your app needs.
Apollo allows you to wrap each component in your app that relies on backend data with a GraphQL api query call. Let’s say you have a “Profile” component that renders user’s name, avatar, date of birth and bio. Somewhere else in the app you have another component “ChatMessage”, that renders message text, author’s name and author’s avatar. You wrap each component with an Apollo higher order component, like this:
export default query(
gql`
user(id: $id) {
name
avatarUrl
dateOfBirth
bio
}
`
)(Profile);export default query(
gql`
chatMessage(id: $id) {
text
author {
name
avatarUrl
}
}
`
)(ChatMessage);
Apollo will take care of sending both queries to your GraphQL server when components are mounted and keeping track of loading progress. It will also cache query results to prevent unnecessary network load on each component re-render.
The idea behind Apollo-link-state is:
- Many components in an Apollo app rely on Apollo cache as source of truth; they are subscribed to it, they have tools to read from cache and to write data into cache if needed.
- But are some pieces of data that are not stored on backend, i.e. local state.
With subscription / notification tooling that we have, why do we need a separate state container altogether? Why not store network data alongside local data in cache?
I won’t dive into technical details here because it’s a subject for a separate article. But here are some charts that explain the concept.
Non-Apollo app:
Apollo app with link-state:
If you are using Apollo, link-state is definitely a strong option for local state management. Though I noticed that in Apollo apps I almost always can do with no state container at all.
And you really shouldn’t be using a state container unless you have to. Here’s why.
Why not state containers?
No matter what library you pick, the concept of state containers itself has one major problem. It creates separation of layers over separation of concerns. Each React app is a hierarchical tree of business logic structures. Pieces of data flow up and down this tree, like this:
If you put some (or all) state related to components into state container, you get this:
If you want to avoid total chaos in your state container, you have to recreate hierarchical tree structure. Then you have to create a tremendous amount of horizontal bridges between the two trees. You need to keep the trees sync but not exactly identical. You need to make double amount of decisions on the data hierarchy. In component tree, these decisions are guided by UI layout and common sense. In state container, they are guided by chance, if you are a bad programmer, and by hours of reasoning about “whether I’m doing it right”, if you are a good programmer.
The biggest problem is: you have to decide whether each bit of data and logic related to a particular component should reside in this component, or it should be part of state container “model”. You won’t be sure what file you need to open in order to fix something. Is it components/root/UserScreen
or store/reducers/User
?
“Huh, this one is simple” — you might say. Global state and logic go to state container, local state and logic stay in component. But can you really distinguish between them? Won’t you regret your decision? Are you sure?
The truth about global state
There’s no such thing as “global state”. It’s a myth. Each bit of state in your app is related to the component it is rendered into. If it is rendered in multiple components, it is state of their first common (grant)parent. If it is something as global as login status, it is state of the root <App />
component. You very probably don’t need no state container. That’s it.
Here are some questions that this article might arise.
Two components use one bit of state, and they are not parent-child. Where do I put this state?
Find their first common (grand)parent. Put the state there. Pass someState
and setSomeState
down as props.
Components are, like, really far away from each other. I don’t want to pass a prop 10 layers down. What do I do?
Use React.createContext
. It looks especially neat with useContext
hook. The times when context used to be “an experimental feature for library developers only” have long passed. You can use context in your app whenever it is reasonable.
How do I handle complex state structures?
Let’s imagine you have a scenario where state of your parent is an object. Child wants to update it, but only partially.
const Parent = () => {
const [counters, setCounters] = useState({counterA: 0, counterB: 0});
return <Child counters={counters} setCounters={setCounters} />
}const Child = ({ counters, setCounters }) => {
const { counterA } = counters;
return (
<div>
<span>Counter A value: {counterA}</span>
<button
onClick={() =>
setCounters({...counters, counterA: counterA + 1})
}
>
Increment counter A
</button>
</div>
);
}
Here we are sort of breaking encapsulation. Child only needs value of counterA
and only wants to update counterA
, but we are having to pass it the entire counters object. In this example it is not critical, but if we had a larger and a more complex object that would clearly be an anti-pattern. We could also carry callback passed to child, but didn’t we want to keep data in the same place as logic?
I said that you probably don’t need state container, but who said that you don’t need reducers? In my code, I’m using them a lot as situational replacement for useState:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT_COUNTER_A':
return {...state, counterA: state.counterA + 1};
case 'INCREMENT_COUNTER_B':
return {...state, counterB: state.counterB + 1};
default:
throw new Error('Unsupported action type');
}
}const Parent = () => {
const [counters, dispatchCounters] = useReducer(
reducer,
{ counterA: 0, counterB: 0 }
); return <Child
counterA={counters.counterA}
dispatchCounters={dispatchCounters}
/>
}const Child = ({ counterA, dispatchCounters }) => {
return (
<div>
<span>Counter A value: {counterA}</span>
<button
onClick={() =>
dispatchCounters('INCREMENT_COUNTER_A');
}
>
Increment counter A
</button>
</div>
);
}
How do I debug?
If your app is well written and well structured, each component has exactly one responsibility — you only need state of one component at a time for debugging. Maybe also states of its children or parents. You can see and manipulate them in React developer tools. Moreover, it is simpler to lookup small data structures as opposed to scrolling through a giant god object in search for necessary props.
How do I do the cool undo/redo thing from that Facebook presentation?
Seriously? Did you ever code an app where you needed to undo/redo the state of the entire application? Probably not. If you need some component to support undo/redo, it can have undo/redo stack that it stores as a ref (equivalent of instance variable for class components).
Example implementation of undo stack:
const [editorState, setEditorState] = useState({text: ''})
const editorStackRef = useRef([])const updateState = (nextState) => {
editorStackRef.current = [...editorStackRef.current, editorState];
setEditorState(nextState);
}const undo = () => {
const editorStack = editorStackRef.current;
const lastItemIndex = editorStack.length - 1;
const lastItem = editorStack[lastItemIndex]; editorStackRef.current = editorStack.slice(0, lastItemIndex);
setEditorState(lastItem);
}
When would you use state containers?
In an app that has no remote backend, or only uses backend for synchronisation. In a very complex app. I wouldn’t use state container for an online shop, not for a messenger, and not even for a social network. What comes to my mind is some sort of online stock trading platform, chart diagram editor or client-only mobile app. I would probably go for MobX-state-tree.
Thanks for reading! 😍
I hope you enjoyed this article. If you have questions or want do discuss I’d be glad to have some nice talk in comments. I’m also available per email: nikis05@mail.ru, and on Telegram: @nikis05.
Please don’t hesitate to tell me if you find any technical inaccuracies. Also, as I’m not a native English speaker, special thanks to those who point to grammar mistakes or stylistic glitches.
Have a nice week and until next time!