This, in contrast to my previous pieces, will be a more opinion based article. So, dear reader, treat everything here with a grain of salt - it's just my feelings, thoughts and ideas related to the problem of state management in React.
Why would you listen to me?
I worked in commercial projects in React that utilized all of the 3 most popular approaches to state management:
- simply using React built-in state mechanisms,
- using Redux,
- using Mobx.
So in this article I will compare those 3 options.
My goal is to present you a balanced opinion on each of this approaches, but more importantly, give a (surely controversial) opinion on why exactly state management became such important problem in React apps, causing people to write countless libraries, articles and conference talks on the topic, that probably should have been solved a long time ago already.
Let's get started!
Origins of state
When I was first learning front-end development, no one talked about "state management". Nobody really cared about state.
In a first commercial app I worked on, written with the immortal jQuery library, people were simply storing state in some random places (like "data-*" property of some HTML element), or not storing it anywhere at all.
In that second case, reading state meant simply checking what is currently rendered in the DOM. Is that dialog window open? There is no boolean telling us that, so let's just check if there is a DOM element with some special class or id in the tree!
Of course this approach resulted in extremely messy and buggy codebase, so the approach of React, where the state of the application is clearly separated from the view, was a huge epiphany for us and it was the moment when the concept of application state was ingrained in our minds forever.
React state mechanisms (both classic and modern)
Since React introduced the concept of state as a separate entity, it also introduced some simple tools to manage that state.
Earlier it was only a setState
method which allowed to modify state stored in a given component. Currently we also have an useState
hook, which has some superficial differences, but ultimately serves the same purpose - defining and modifying state on a per component basis.
Now this last information is the key here. In React each piece of state is defined "inside" the component. So not only a hypothetical component FirstComponent
will have a state independent from the state of SecondComponent
, but even each instance of FirstComponent
will have it's own instance of state. This means that (at least out of the box) there is no sharing of state between React components. Each has its own state instance that it creates an manages and that's it!
But it turns out that we quite often want to display the same state in different places of the website (and hence, in different components).
For example the number of new messages in Facebook header at the top of the application should be always equal to the number of unread messages at the bottom, in the messenger window itself.
Having a shared state - a list of messages, some of which are marked as "unread" - would make that trivial, ensuring that both components always show the same information.
Messenger
component would simply display the messages from the list, marking the unread ones with a bold font. At the same time Header
component would count how many messages are marked as unread on the list and would display that number to the user.
As an alternative, having two separate copies of that state - one in Header
component and one in Messenger
component - could result in those states getting out of sync. User might see for example that there are two unread messages in the Header
, but then he would not find any unread messages in Messenger
. That certainly would be annoying.
So how would we achieve state sharing, using only React, without any additional libraries?
A canonical way to share state is to store it in a single component, somewhere higher in the component tree. Then you can simply pass this state down as props. So you can pass the same state to two separate components via props and... boom! Those two components are now sharing that state.
This works very well at the beginning. But if you write your applications this way (and if they get complex enough) you will quickly notice that a lot of your state "bubbles up" as the time goes on.
As more and more components need access to the same state, you put that state higher and higher in component tree, until it finally arrives to the top-most component.
So you end up at some point with one massive "container" component, which stores basically all of your state. It has tens of methods to manipulate this state and it passes it down to tens of components via tens of props.
This quickly become unmanageable. And there is really no clean or easy way to somehow divide this code into smaller pieces. You end up with one massive component file, that often has more than a thousand of lines of code.
You end up with a similar mess as you had before you used React to separate out the state from the view. Yikes...
Redux to the rescue
Redux was invented for a bit different reason than what we described above. In fact, it was conceived purely as a presentation tool, to show the potential of "time travel" in developing React applications.
It turns out that if you put all of your state in one place (called "the store") and you always update it all in one step (using a "reducer" function), then you basically get a capability to "travel in time". Since you can serialize the state you keep in your store and save it after every update, you can keep the history of all the past states.
Then you can simply come back to any of those past states on command, by loading them back to the store again. You are now time traveling - you travel back in time in the history of your application.
Time travel was conceived as a method that would help to develop and debug React applications. It sounds great and people flocked to the idea immediately.
But it turns out that this capability is not as useful as people initially thought. In fact, I believe that most of currently existing Redux applications do not utilize time travel in any significant way, even for debugging purposes. It's simply too much hustle for what is worth (and I am still a big believer in console.log
-based debugging).
There is however a quality of Redux that, I believe, made it a staple of programming complex React applications since the very beginning.
As we said, the state in Redux is not created anymore on a per-component basis. Instead, it is stored in a central, in-memory database, called - as we mentioned - the store.
Because of that, potentially any component has access to this state, without passing it down via props, which is simply too cumbersome. In Redux, any component can access the store directly, simply by using a special utility function.
This means that any data that you keep in the store can be displayed, with very little effort, in any place of your application.
Since multiple components can access the state at the same time without any issues, state sharing also stops being a problem.
Our Facebook website can now display the number of unread messages in any place we want, provided we keep the list of messages in the store.
Storing all the state in one place might sound a bit similar to how we kept all the state in a single component. But it turns out that, since updates on Redux store are done by reducer functions, and functions are very easily composable, dividing our Redux codebase to multiple files, split by domain or responsibilites is also much easier than managing one massive "container" component.
So Redux really sounds like a solution to all of the problems that we described before. It might seem that state management in React is solved and we can now move on to more interesting problems.
However, as it is in life, the truth is not that simple.
There are two more pieces of Redux that we did not describe yet.
Although the components can read the Redux store directly, they cannot update the store directly. They have to use "actions" to basically ask the store to update itself.
On top of that, Redux is conceived as a synchronous mechanism, so in order to perform any asynchronous tasks (like HTTP requests for that matter, which is not a crazy requirement for a web app), you need to use a "middleware" which grants your Redux actions asynchronous capabilities.
All of those pieces - the store, reducers, actions, middleware (and a whole bunch of additional boilerplate) make Redux code extremely verbose.
Often changing one simple functionality in Redux results in modifying multiple files. For a newcommer it's extremely difficult to track what is happening in a typical Redux application. Something that seemed simple at the beginning - storing all the state in a single place - quickly turned into extremely complex architecture, that takes literally weeks for people to get used to.
People obviously felt that. After the success of Redux, there was a massive influx of various state management libraries.
Most of those libraries had a thing in common - they tried to do exactly the same thing as Redux, but with less boilerplate.
Mobx became one of the more popular ones.
Magic of Mobx
In contrast with the focus of Redux on functional programming, Mobx decided to unapologetically embrace old-school Object Oriented Programming (OOP) philosophy.
It preserved Redux's concept of the store, but made it simply a class with some properties. It preserved Redux's concept of actions, but made them simply methods.
There were no longer reducers, because you could update object properties like you typically would in a regular class instance. There was no longer a middleware, because methods in Mobx could be both sync and async, making the mechanism more flexible.
Interestingly, philosophy remained the same, but the implementation was vastly different. It resulted in a framework that - at least at the first glance - seemed more lightweight than Redux.
On top of that, Mobx was speaking the language much more familiar to regular software developers. Object Oriented Programming was part of a typical programmers education for decades, so managing state in terms of classes, objects, methods and properties was much more familiar to the vast majority of programmers getting into React.
And once again it might seem that we have solved our problem - we now have a state management library that preserves the ideas and benefits of Redux, while being less verbose and less alien to newcommers.
So where is the problem? It turns out that while Redux is openly complex and verbose, Mobx hides it's complexities, pretending to be a programming model that is familiar to majority of developers.
It turns out that Mobx has more in common with Rx.js or even Excel than traditional OOP. Mobx looks like Object Oriented Programming, while in fact it's core mechanism is based on vastly different philosophy, even more alien to regular programers than functional programming, promoted by Redux.
Mobx is not an OOP library. It's a reactive programming library, sneakily hidden under the syntax of classes, objects and methods.
The thing is, when you are working with Mobx objects and modifying their properties, Mobx has to somehow notify React that a change to the state has occured. In order to achieve that, Mobx has a mechanism that is inspired by reactive programming concepts. When a change to the property happens, Mobx "notifies" all the components that are using that property and in reaction those components can now rerender.
This is simple so far and it works flawlessly, being one of the reasons why Mobx can achieve so much of Redux's functionality with so little boilerplate.
But reactiveness of Mobx doesn't end there.
Some state values depend on others. For example a number of unread messages depends directly on the list of messages. When a new message appears on the list, the number of unread messages should in reaction increase.
So in Mobx, when property changes, the library mechanism notifies not only the React components displaying that property, but also other properties that are depending on that property.
It works just like Excel, where after you change the value of one cell, the cells that depend on that value are in reaction immediately updated as well.
Furthermore, some of that properties are calculated in an asynchronous manner. For example if your property is an article id, you might want to download from the backend the title and author of that article. These are two new properties - title and author - that directly depend on a previous property - article id. But they cannot be calculated in a synchronous way. We need to make an asynchronous HTTP request, wait for the response, deal with any errors that might happen and just then we can update the title and author properties.
When you start to dig dipper, you discover that Mobx has plenty of mechanisms and utilities for dealing with those cases and it is a style of programming that is explicitly encouraged by Mobx documentation. You start to realize that Mobx is only Object Oriented on the surface and is in fact governed by a completely different philosophy entirely.
What is more, it turns out that this graph of properties and their dependencies quickly becomes surprisingly complicated in a sufficiently big application.
If you have ever seen a massive Excel file that is so big and complicated that everyone is too scared to make any changes to it - you have basically seen a Mobx app.
But on top of that, Mobx reactiveness mechanism is not directly accessible or visible to the developer. As we said, it is hidden under OOP syntax of classes, methods and decorators.
Because of that a lot of what Mobx does is simply "magic" from a programmers perspective. I have spent many hours scratching my head, trying to figure out why, in a certain situation, Mobx's mechanism does (or doesn't do) some updates. I had moments where my code was mysteriously sending multiple HTTP request instead of one. I also had moments where my code wasn't sending any request, even though I could swear it should.
Of course in the end the errors were always on my side. Mobx works exactly as it should.
But while Redux is complex because it basically gives all the pieces into your hands and asks you to manage them, Mobx does the exact opposite, by hiding it's intricacies from you and pretending it's just a "regular" OOP library.
One approach causes the code that is full of boilerplate, multiple files and difficult to track relations between different parts of the codebase.
The second approach causes the code that looks slim and elegant, but then from time to times it does things that you don't expect and are difficult to analyze, because you literally don't understand what the library does underneath.
The lie of state management
Interestingly, this whole article was written under the premise that shared state is a common requirement of many modern web applications.
But... is it really?
I mean, of course, you will sometimes have to display a number of unread messages in two completely different places in your application.
But is that really enough of a reason to create a complex state management solutions?
Maybe... maybe what we need is literally just a way to share state between components in a manageable manner?
I am imagining having a useSharedState
hook, which would work just like a regular React state hook, but would allow components to access the same state instance, for example by sharing a predefined key:
const [count, setCount] = useSharedState(0, "UNREAD_MESSAGES_COUNT");
In fact this idea not new at all. I have seen at least a few implementations of a hook similar to this one.
It seems that people are (consciously or not) feeling the need for this kind of solution.
Of course it doesn't solve all of the problems yet. The biggest one being that asynchronous code (in particular data fetching) is still incredibly awkward in modern React and implementing it in modern hook syntax feels almost like a hack (in fact, I will probably write a follow up article on that exact problem).
But I will still hold my controversial claim which I promised you at the beginning of the article:
All this mess with state management debates, thousands of libraries created and articles written, stems mostly from a single reason - there is no easy way in React to share state instances between components.
Now bear in mind - I never had an occasion to write a full, commercial application using this hypothetical useSharedState
hook. As I mentioned, there would be still some things needed to make such an application really easy to develop and maintain.
So everything I say now might be completely misguided, but I will say it anyway:
We over-engineered state management in React.
Working with state in React is already close to being a great experience - separating state from the view was a huge stepping stone - we only lack a few little solutions to very specific problems, like sharing state or fetching data.
We don't need state management frameworks and libraries. We just need few adjustments to the core React mechanism (or simply a few tiny utilities in an external library).
Writing our massive web applications will always be complicated. State management is hard. In fact, the bigger your app is, the exponentially harder it becomes.
But I believe that all this time and effort that goes into learning, debugging and taming state management libraries could be instead devoted to refactoring your application, architecting it more carefully and organizing the code better.
This would result in a code that is simpler, easier to understand and easier to manage by your whole team.
And I see that this is a turn that React community is already slowly doing, being more and more vocal about being disappointing by programming with Redux or Mobx.
So... what do I use TODAY?
Of course Redux and Mobx still have their place. They are trully great libraries. They solve very concrete problems and bring specific advantages to the table (and specific drawbacks at the same time).
If you want to dabble into time traveling debugging or you need to store your serializable state in one place (for example to save it on the backend or in local storage), then Redux is for you.
If your applications state is highly interconnected and you want to make sure that updates of one property will result in immediate updates of other properties, than Mobx model will fit that problem very well.
And if you don't have any specific requirements, just start with vanilla React.
I described some issues with "vanilla React" approach in that article, but it is a completely different thing to encounter those problems by yourself in practice. Having this experience, you will be better informed to make a smart decision on which state management solution to choose.
Or not choose. ;)
If you enjoyed this article, considered following me on Twitter, where I am regularly posting articles on JavaScript programming.
Thanks for reading!
(Cover Photo by Felix Mittermeier on Unsplash)
Top comments (26)
"there is no easy way in React to share state instances between components"
React Context lets you easily share state in all components, doesn't it? This is a common solution to state management that uses built-in React features.
No - it does not let you easily share state in all components. It let's you easily share state in all components that are descendents of the context provider.
Personally, I love context. It allows me to think of controllers as components. I don't have to deal with the boilerplate of redux and I don't have to deal with the magic of mobx.
But, if I'm on a team of developers that have various levels of experience in react, I would almost always go for redux as long as an experienced senior is driving the redux bus. If everybody on the team is experienced seniors in react, then context is a-ok by me.
Having to be a descendant of a context provider isn't a huge limitation or drawback for context. But I'm sure there are some scenarios I could be missing where that's an issue.
I didn't say, nor imply, it was a drawback. It's not. It's actually a huge feature. React is still, even after several years of composable UI, a "new way of doing things" to a lot of devs and it's hard (or impossible) to grok it all at the outset. To say something like "context lets you share state across all components" is a confusing statement to the ones who haven't made their own mental models yet when they try it and it fails, or they nest context providers without realizing it and end up in a really uncomfortable state because they can't connect "context = global shared state" with what they're seeing with they're own eyes.
Ah I gotcha. I read your original comment wrong. Thanks for clearing that up for other readers 👍
I made an account just to comment on this.
The whole time reading this article I was thinking "WHAT ABOUT USECONTEXT????"
This is the third article I've read today that discussed the struggle of shared state in react, and didn't mention the useContext hook. Is there something I'm missing?
I dont do huge apps, but Mobx worked really well and I haven't run into any of the mentioned issues. I split state into multiple Mobx stores, organized hierarchically. So only RootStore usually gets pushed through provider and other stores are accessible through it. Also parent store is referenced by child store so you can go up the hierarchy too. Basically, its what Mobx docs suggest: mobx.js.org/best/store.html
I must say I also haven't had any unexpected behavior. Quite oposite. "Scratching my head" with React usually was about why some component was rerendering more than once, or even twice. I'm really careful about optimization there. Hate to see rendering time wasted. I find that goes away with Mobx. React state, Memo, and other ways to fight it are in my opinion much more complex thatn Mobx. In Mobx, all you have to remember basically is "dereference values as late as possible" (one point from Mobx docs: mobx.js.org/best/pitfalls.html)
Hey Mirko. Thanks for commenting.
Maybe that wasn't entirely clear in the article, but I DO consider Mobx superior to Redux in most of use cases.
I didn't have "scratch my head" moments too often when developing an app, but I still felt that I am not fully in control of the framework.
This article wasn't meant to bash Mobx (or even Redux). If it works well for you, by all means continue to use it.
No, no, I haven't foind your article to bash Mobx. Just focussed on Mobx part of it, trying to find myself there, but didnt :). And wanted to share.
Mobx is a "magic black box" often, but I got so used to it that it doesn't serve me any surprises.
Fair enough!
Glad it works for you. Thanks for the comment. :)
I read the whole article, it made a lot of sense to me as we did face the same issues with redux and simiarly mobx. Good job!
For our app, which was made via knockoutJS, have very similar concept to mobx, namely the pub-sub pattern and the computed functions. Just like you said, a lot of complexity comes from asynchronous functions coupled with mobx. So I am wondering what do you think if we structure the app in the way that we will have components handle the service calling, not mobx. This way we take out the step that causes complexity ?
I think you may end up with some problem later unless the app is very small. If you put the fetch logic in the component it is hard to share if needed by another component (that is, another component must be able to fetch this data and update the store). If you then move it outside of the component into a helper you are very close to a thunk or action any way but it is outside the components and the store and therefore a bit harder to find. One nice thing with mobx is that almost everything about state and actions can be found in the store.
It could also make writing tests harder. When everything is in a mobx store you can write tests for it. If you split it into a store, some helper and a component you have to test them all combined to know that the application works.
I agree with your observations, namely that if everything's inside the store then you'd have a centralized place for everything. However, I think that's exactly the drawback of the. We had a relatively small app that used redux and had all types of data, global state and service responses, stored in there.
It quickly became a nightmare when there are scaffolding after scaffolding for getting stuffs, which was hard to explain to our team lead who had no hand in building it. It felt as if walking in a labyrinth inside the store. If everything was separated completely, store only has global state, service layer is the controller, cache layer is another, then it would be much clearer.
as for writing tests, you may very well be correct, but I am too newbie at test-writing to comment on :P
Hey Kevin, thanks for the reply.
I like your point about async stuff bringing a lot of trouble to Mobx.
Moving async stuff to components definitely might work nicely. My only fear would be only that it would again turn into "Redux-esque" architecture, where whenever somebody new comes to the project, you have to explain to them "oh, we don't put async stuff in Mobx stores, we put it in components, blablabla...".
But maybe that's not a huge issue and if everyone in the team is on the same page, I think it would be completely fine to try that approach.
awesome thanks!
What do you think about Recoil? Recoil enabled me to manage states in a similar way to React. It requires much less boilerplate code than Redux, and it also integrates well with React.Suspense for asynchronous code.
I haven't really used MobX before so I can't speak about it though.
Hey Makoto.
Never used Recoil, in fact never seen anyone using it.
But you got my interest, I will definitely check it out. Thanks!
I'm a newbie but have been using Recoil on a multi-screen app. Works great for me so far and fairly easy. Maybe too easy.
I really like the idea of a
useSharedState
concept, and I recently attempted that and created a very small library for it in: github.com/sammysaglam/react-stateyI don't any reason why something like this wouldn't be great & simple! And to address asynchronous behavior, I call
setState
in auseEffect
, which I think is also quite easy to handle & read.Thank you for writing this. I finally understand the core differences between Redux and MobX.
Regarding
...have you seen ReactQuery or Vercel's SWR?
They're two very similar libraries. I think you'll like them.
I mention them in some of my posts.
Thanks again!
Dude, try "react easy state" it's the way to joy and peace.
Will check it out, thanks!
In additional to the docs here github.com/RisingStack/react-easy-...
this is an interesting read to blog.risingstack.com/reinventing-h...
Some comments may only be visible to logged-in visitors. Sign in to view all comments.