Nowadays it is very common to find two different kind of things related to state management in React Apps. “React + Redux as the way to manage state in react apps” and on the other hand many posts with a title like “You Probably don’t need Redux”.
And obviously it is completely wrong to say that this represents the full range of possibilities when it comes to state management on React apps. However, the last title is totally correct and it is because you might not need redux at all. Or more importantly, it is necessary to understand when we need it and when not so as not to condemn the future of our apps.
I’ve seen people add Redux to their dependencies without even thinking about it, and I’ve been one of those people. This is no rant about Redux being good or bad, of course Redux is great that’s why so many people have been using it for years now. The aim of this writing is instead to share my opinions on the use of Redux or any other state management library and where does it make sense to use those and more importantly where it doesn’t.
React’s Context API has been around for some time now and it is good, useReducer is great as well, but that doesn’t make Redux obsolete. Redux still makes sense for me. Let not size be the parameter for using or not using Redux. It is not about the size of the application, It is about the state. That is also where the pain begins. In large applications with multiple developers working on the same codebase, It is easy to abuse Redux. You are just a bad choice away and then everyone starts pushing anything and everything into Redux store. I’ve been one of those people, again.
And obviously I understand that the latter sounds weird and counterintuitive when talking about redux. “What do you mean by abusing Redux? It is meant to be a global data store right?”
Yes, It is meant to be a global data store but the term
global data store is often translated as a state to hold every state, value, data and signal. That is wrong, and It is a slippery slope, goes 0 to 100 quite fast and soon enough you find yourself working on an application with absolutely messed up global state, where when a new guy onboards he doesn’t know which reducer to use data from because there are multiple copies or derived states.
People often get used to the fact that they’ve to make changes in 3 files whenever something changes, why??? That’s a pain, we’ve got accustomed to and as the size of application or scope increases this only gets worse. Every change becomes incrementally difficult because you don’t want to break existing things and you end up abusing Redux further.
When that stage comes, we often like to blame React and Redux for the meaty boilerplate it asks of you to write.
“It’s not my problem or my code’s problem, that’s just how Redux is…”
Yes and No. Redux definitely asks you to write some boilerplate code, but that’s not a bad deal until you overuse it. As often said, nothing good comes easy and so is with Redux. You have to follow a certain way that involves -
- The usage of a shared and centralized store to keep the application state as plain objects.
- The definition of actions with the purpoise of maintaining changes in the system in form of plain objects.
- The definitions of reducers that describe logic to handle state changes in terms of objects.
In my opinion, that’s not a very easy pattern to follow and introduce in your applications. The steepness of this curve should deter abuse of Redux and make you give some more thought before opting Redux .
React gives you local state. Let’s not forget it and use is as much as possible before looking for ‘global’ solutions, because if you pay close attention most data in your Redux store is actually just used by one or two components. ‘global’ means much more than one or two components right?
In the following sections we are going to try to analyze the different aspects in which the decision we make when defining the state management of our application affects us.
The first try for most of developers trying to store data and manage an state is to centralize it and make it global. And yes, that’s what Redux is for, maintaining your global state, but I’m really sure that if we pay close attention to the data that we want to manage we will find values that are required just by a single component but you thought that someone might need them in future in some other component so why not put in the effort to put this local data in Redux. That’s where we are often wrong. Because the chance of a future guy requiring the same data in some other component is really low and even if that happens, chances of duplication of data and derived Redux states are good. Over time, this practice of putting values unnecessarily in Redux store becomes perfunctory, and you eventually land in a big stinking mess of reducers and states where nobody wants to touch anything and they’d rather create a new reducer in fear of causing regressions in god knows which component. I know proper code reviews and processes in place will not let the situation get that dire, that fast but definitely the morning will come, when the realisation strikes and all you are left with is an insurmountable tech debt of fixing state management in an existing code base with minimum regressions.
So heed my words — whenever you are thinking of going the Redux way with a state, give a good thought to it. Is that data really of global use, are there other (non-immediate child) components which require that data?
This is why I think it is necessary to think before proposing a solution and at the same time add new dependencies only when they are really needed.
It is always better to consider step by step each of the modules that will be needed and what data will be necessary for this module to work in the best possible way. At the same time, it is important to take into account how many times we will be writing the data that we have to store for said module and how many readings it will have. Most of the time the number of writes is really less and there is no problem in persisting the data in the local state of some component that then passes this data to its children. This becomes very simple using the React Context API and allows us to encapsulate a lot the responsibility of the data and how it is handled.
Redux mostly gets looped into the data fetching scene in the React world but why? Why do you have to write an action, reducer every time you want to make an API call? It is an overkill if you start doing this from the very beginning of your application. Redux is meant to store values/data which might be used by multiple components in your application, possibly on different component trees. Data fetching from an API to fill up a component isn’t really something that fits that definition, why should data fetched from an API to be used by a single component go through store to the component in question?
However many people prefer not to have data fetching logic in their components. We don’t want to pollute our components with data fetching logic…
I am all for clean and readable code, and readability principles demand that one should be able to comprehend what is happening in your code as easily as possible (implies — with opening as less files as possible), now keeping that in mind, I don’t think having your data fetching logic specific to a component inside that component is polluting it. It is actually making your code readable and understandable as probably the most critical part of your component, the data it fetches and how it fetches it is quite conspicuous to the reader, isn’t it?
However, it is not necessary to put your fetch calls inside the components just as they are. There are different solutions that allow you to abstract this, which are seemingly easy to use and do not take away the brevity and readability of your code.
Again, going step by step allows you to define which patterns to use carefully and judiciously. For example, if I know that I am going to need data from a user and this data is only read by the component that makes the fetch call and its children, I can store these in the local state of my component and pass it as props, or use the React Context API to avoid explicitly passing props to these child components. There are ways to abstract this that we will see later.
At the same time, it is really common to think: Since Redux is a global store, we don’t have to fetch data again…
Some of you might be having this as an argument. Well, most of us make API calls to fill up our components whenever the component “mounts” and that data comes via Redux, right? So until you have proper data validation mechanisms to know that your Redux store needs to be repopulated with fresh data from an API call, you are going to make that API call on every “mount”, so how are we saving anything there? And if what you really want ‘caching’ so that you can save on your API calls, why not go with solutions built for that, instead of trying to mould Redux into your cache.
There are a lot of brilliant libraries out there to help you tidy up your data fetching. SWR is one amazing utility to get started with. If you want to take it a notch up, you can consider going with react-query both of these are mostly based around render time data fetching and provide great ways to optimise on your data fetching operations.
Sometimes you might need event based data fetching — fetching data when some particular ’event’ happens. For such cases, it is good to think about using specific libraries for that or create custom react hooks with the only purpoise of solving the problem you have and not any problem that you don’t.
I’ve been walking this path of avoiding Redux for data fetching for some time now, and I am quite happy with the results. Most performant and most maintainable lines of code are the ones not written, and avoiding Redux in your data fetching can save you a lot of time, code with almost no harm.
Redux for future applications
In most cases, teams opt for Redux because some senior members on the team think they might need it in future. And this often becomes a very serious mistake later on, because It is not easy to rollback on such decisions. You and possibly your team might be taking future and especially Redux as part of the React world too seriously. As our prophet and co-creator of Redux, Dan Abramov has said -
Also he has said that You Might Not Need Redux knowing that Redux has some use cases that probably you don’t need for your app or you don’t know it yet.
Redux is just another library, albeit a great one. Use it when you identity the need for it, not just because it is such a major name in the React ecosystem.
I think it is right time to share the unheard wisdom — Nobody knows the future, Not even the seniors — all the time. Future depends on present (genius!) and the decisions we make today reflect tomorrow. It is okay to ask why, before adding anything to your bundle. Remember as Front-end engineers, user is the King and every avoidable package you use adds to that bundle size, making it heavier and ultimately slower to load.
How to manage state in new apps?
I think this is something that can be inferred from everything mentioned above, however I’ll take the opportunity to give you some notes on how I think about these types of questions step by step as problems arise.
I prefer to solve problems as they arise in terms of state management. This does not mean that I cannot imagine what I will need in the future. Obviously I can. I’ve been doing things in React for a long time now and this allows me to get an idea of how to define a new application from the precise moment that I have the requirements for it. However, implementing the solution and start coding is not the first thing one should do. First it is necessary to define the different modules that we are going to have, the architecture that will allow us to arrive at the best possible solution and then go iterating. It is in this process of defining the modules that we can detect the type of functionalities and data that we are going to need. For example, if my system needs to distinguish between different users, it is fair that we have a module that allows our users to authenticate. That’s when we define the input data for this process and at the same time what data can I get from it. For example, I can think that given a user email and password, I can obtain a token that I will use in all future fetch calls.
That is when I ask my self which other modules will need this token and have the contraint of the user being authenticated? and which ones are not?. Most likely, in this case, you only have one writing of this data, at that precise moment when the user logs in, and several reads in the child components. In this case I do not have any apparent problem that indicates that I cannot share this data due to the flow of props of that subtree of components.
That is why I can think of defining a component that keeps this data in its local state and gives a context about the use of this data.
Later I define how I am going to make the child components access this data. I particularly like the use of react hooks for this kind of thing, but any other solution is viable for those who don’t prefer it.
I will prefer to implement this solution in such a way that it allows the user to abstract from my new component and hooks about how it is being implemented, since I hope that this module is as self-contained and black box as possible in order to be able to reuse it if it is necessary. This allows me to focus on the implementation of the module, its testing, its documentation and later on examples of use that allow it to be used in future opportunities without major changes to the architecture in question.
If I have a problem that I need to solve in that specific module, I don’t need to debug it using a debuggers like Redux Devtools since my state is not centralized and I have well-defined responsibilities.
This way I avoid adding noise to my applications, modularizing and distributing responsibilities as problems arise.
We don’t use libraries for state management like Redux anymore?
This is not the case, the objective is not to refuse outright the use of libraries, but to wait to have some problem to solve and to know that the use of a library is going to give us more advantages than anything else.
At the same time it is very important to understand different issues when you get to that point. Redux is not the only thing that exists, and this is because the concept of a centralized state is not the only concept that we can apply when we want to solve problems.
There is a great variety of flavors that allow us to solve our problems if we have the criteria and sufficient definition of it to be able to apply them.
For example, there is also the idea of a set of distributed states that would allow solving some use cases for which redux is commonly used but that ends up being limiting in terms of performance. For this there are very small and simple libraries such as Jotai and Recoil. Both options are very interesting and allow to have a very simple, understandable and scalable code. I plan to write a bit about these libraries in the future, so stay tuned!
I saved the best for the end.
Hookstate is currently my favorite React state management library, even though it’s the least popular in the group. It’s small, minimal, clean, extendable, and it has a lovely hook-based API.
This one might be for you, but only if you’re — like me — in love with React hooks. Hookstate utilizes them, and a couple of other impressive techniques to deliver great development experience and performance.
Not only can it be used for global state, but can also enhance local `useState()` with additional features, handle nested state without performance loss, and deal with async data with ease!
All that and more in a small package, with simple but also easy-to-use plugin architecture for even more features.
I highly recommend you check it out!
Anyway, at this point, as we’ve gone through all these great libraries, I’d like to remind you one last time that you don’t necessarily need them.
So, State and Context API — that’s the default. No Redux or even Recoil that’s coming directly from Facebook. Only opt-in for an external library when you’re 100% sure you’re going to need it, or right when it’s needed. That’s partially why I like Hookstate so much. It provides a lot of additional features with a pleasant API while having the smallest footprint of the bunch. That’s the closest I can comfortably get to a “stock React” solution.