React revolutionized front end development as most people knew it when it was first released. This new approach to writing code triggered incredible innovation in how to handle state changes and UI updates.
This revolution had its downsides, too. One of them was a culture of over-engineering solutions to challenges that could be solved in simpler ways. A typical example of this is how state has been managed in React applications.
Redux has become a hallmark of many React applications created in the last couple of years. The allure of having a single state object, available everywhere in your application sure sounds nice. But has its time passed? Has React evolved to a point where these kinds of state management tools add more complexity than they solve?
This article aims to give you a deeper understanding of which situations warrants state management tools like Redux. We’ll discuss the reasons behind the rise of Redux, and what has changed in the last couple of years - both in React and in Redux. Finally, we’ll look into what might be coming in the future.
Redux - and why people started using it
When it was first released , React didn’t have an officially supported way to pass data far down the component tree. If you had some kind of shared state, configuration or other information you would like to use anywhere in you application, you had to pass it down from parent to child to sibling to another child. There was a way to avoid it, but that way - the “legacy context API” was never officially supported, and was documented with a warning that it should not be used.
About the same time React was released to the public, some other Facebook engineers introduced a blueprint for how they created front end applications - the Flux architecture. It complimented React’s component-centric design by having a unidirectional data flow, which made things both easy to follow and simple to understand.
(photo borrowed from https://facebook.github.io/flux/docs/in-depth-overview)
While many famous open sorcerers were busy fighting over which slightly different implementation of this was the best, a young Russian developer named Dan Abramov introduced an implementation based on the Elm architecture, called Redux.
Redux was a pretty simple system, with a single state object, encased in a “store”, which could be updated by dispatching actions on it. The actions were sent to a “reducer” function, which returned a brand new copy of the entire application state, which would then propagate across your application.
Another great feature of Redux was how easy it was to use with React. Not only was it a great match with the programming model of React, it also solved the prop drilling issue! Just “connect” whatever component you want to a store, and you had access to any part of the application state you wanted. It was like magic!
Context, hooks, and why it solved much of what Redux did
With all its elegance and popularity though, Redux did have a few major downsides. For each new way of changing the state, you had to add a new action type and action creator, probably a dispatcher and a selector, and then you’d have to handle that new state change in an existing reducer, or create a new one. In other words - lots and lots of boilerplate.
When the 16.3 version of React was released, it finally shipped with a fully redesigned context API. With this new feature, prop drilling was suddenly as easy as wrapping any subsection of your application in a context provider, and fetching it again with a context consumer component. Here’s an example of how that could be done:
const UserContext = React.createContext();
class MyApp extends React.Component {
state = { user: null };
componentDidMount() {
myApi.getUser()
.then(user => this.setState({ user }));
}
render() {
return (
<UserContext.Provider value={this.state.user}>
<SomeDeepHierarchy />
</UserContext.Provider>
);
}
};
const UserGreeting = () => {
return (
<UserContext.Consumer>
{user => ( // look - no Redux required!
<p>Hello there, {user.name || 'customer'}!</p>
)}
</UserContext.Consumer>
);
};
At ReactConf in 2018, now React Core team member Dan Abramov and boss Sophie Alpert introduced a new feature in React - hooks. Hooks made using state and side effects much easier, and made away with the need for class components altogether. In addition, the context API was suddenly much easier to consume, which made it much more user friendly. Here’s the revised code example with hooks:
const UserContext = React.createContext();
const useUser = () => {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
myApi.getUser().then((user) => setUser(user));
}, []);
}
const MyApp = () => {
const user = useUser();
return (
<UserContext.Provider value={user}>
<SomeDeepHierarchy />
</UserContext.Provider>
);
};
const UserGreeting = () => {
const user = React.useContext(UserContext);
return <p>Hello there, {user?.name ?? "customer"}!</p>;
};
With these new features landing in React, the trade-offs for using Redux changed quite a bit. The elegance of reducers were suddenly built into React itself, and prop-drilling was a solved challenge. New projects were started without having Redux in the stack - a previous no-brainer - and more and more projects started to consider moving away from Redux altogether.
Redux Toolkit and hooks - a new and improved user experience?
As a response, the team currently maintaining Redux (led by a gentleman named Mark Erikson) started two different efforts. They introduced an opinionated toolkit named Redux Toolkit that did away with most boilerplate code through conventions, and they added a hooks-based API for reading state and dispatching actions.
Together these two new updates simplified Redux codebases substantially. But is it really enough to defend introducing the added complexity of the concepts in Redux to a new project? Is the value Redux adds more than the added cost of teaching new employees about Yet Another Tool?
Let’s look at where React does a great job by itself, and in what cases the tradeoff of complexity vs power is worth it.
When React is enough
Most React applications I’ve worked with have been pretty small in scope. They’ve had a few global pieces of state that was used across the application, and some data that was shared across a few different views.
Besides from this though, many React applications don’t have a lot of shared state. Most state like the content of input fields or whether a modal is open, is only interesting to the component that contains them! No need to make that state globally available.
Other pieces of state might be shared, but only by a part of the application. Perhaps a particular page requires a piece of state to be shared across several of its components, or a sidebar needs to expose some remote status to all of its children. Either way, that’s not global state - it’s state scoped to a part of the application.
By keeping state co-located, or as close to its dependents as possible, you ensure that it’s deleted whenever the feature requiring it is deleted, and that it’s discoverable without leafing through tens of different reducers.
If you need to share app-wide settings that rarely change, React’s context API is a great tool to reach for. One example of this is what locale is currently active:
const LocaleContext = React.createContext({
locale: "en-US",
setLocale: () => {},
});
const LocaleProvider = (props) => {
const [locale, setLocale] = React.useState("en-US");
return <LocaleContext.Provider value={{ locale, setLocale }} {...props} />;
};
const useLocale = () => React.useContext(LocaleContext);
Other use cases can be what color theme is active, or even what experiments are active for a given user.
Another very useful approach is using a small data-fetching library like SWR or React-Query to handle fetching and caching your API responses for you. To me, cached data isn’t really global state - it’s just cached data. This is much simpler to handle with these small single-use libraries, than introducing async thunks or sagas to your Redux rig. Also, you don’t have to handle all the complex variations of isLoading, hasError and what not. With these libraries, it works out of the box.
A thing these context use cases have in common is the fact that they represent data that rarely updates. Rarely in the context of computer science is a bit vague, but in my mind, less than a couple of times every second is pretty rare. And as it turns out, that’s the way the React Context API works best!
The use cases summarized above covers most of the situations I’ve met in real world applications. Actual global state is rare and far between, and is often better off being co-located with the code that actually uses it, or provided through the context API.
Situations where Redux might be warranted
With all that said, Redux is still a great product. It’s well documented, adopted by many, and can be combined with the approaches posted above. But what use cases warrants the added complexity and learning curve of adding Redux to your stack in 2021?
One of the use cases I see most in the projects I’m involved with is when you have advanced data fetching scenarios that requires a lot of cascading network communication. One might argue that this is best done on the server side, but there are definitely use cases where handing this on the client is warranted. Redux, particularly in combination with so-called thunks, is extremely versatile and flexible when it comes to such orchestration.
Another use case is for very interdependent states, or states that are derived from several other states. This is possible to handle in React as well, but the end result is still much easier to both share, reuse and reason about in Redux.
A third use case is for those where the state of your application can change very rapidly. The lead architect of React, Seb Markbåge, stated a few years ago that the current implementation of the context API was suboptimal for sharing data that updated quickly, since a change in the context-provided value would trigger a re-render of the entire subtree of components. Web socket driven trading or analytics dashboards might be good examples of such a situation. Redux gets around this by only sharing the store instance through context, and triggers re-renders more explicitly.
A final use case is highly subjective, and is for teams that enjoy the top-down single-state-tree approach. That the entire state of the application can be serialized, de-serialized, sent over the wire and persisted in local storage. That you can time-travel across changes, and provide a full story of actions leading to a bug to a bug tracking tool. These are powerful arguments, and definitely a value-add for some.
Measuring Redux performance
Monitoring the performance of a web application in production may be challenging and time consuming. Asayer is a frontend monitoring tool that replays everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder.
Asayer lets you reproduce issues, aggregate JS errors and monitor your app’s performance. Asayer offers plugins for capturing the state of your Redux or VueX store and for inspecting Fetch requests and GraphQL queries.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
The other options
In my opinion, most applications can do without external state management libraries. Some disagree, and some have such advanced use cases that handling it without some kind of intermediary layer is very unpractical. In such cases, I suggest you look into Redux’ competition, before landing on the tried and true alternative.
MobX is a well-tested and popular state management tool that works through the magic of observables. It’s quick as heck, and most people that try it become fans within weeks. I haven’t tried it myself, so I won’t be advocating for it too strongly, but the design looks solid!
Another contender is Recoil. This library also stems from the engineers at Facebook, and is based around the concept of atoms of state, and derived state called selectors. It’s very similar to React in its API design, and works flawlessly with it. It’s currently in an open beta, but it should still be useful in many projects.
The final alternative I want to suggest is Overmind. Overmind is the state library that runs the main editor application over at CodeSandbox, and is based around a single state tree and side effects. It’s also something I’ve never tried before, but by looking at the complexity and lack of bugs in CodeSandbox, it must be pretty powerful!
Even with all of these alternatives present, Redux is still holding its ground. With the recently added hooks and Redux Toolkit, the developer experience has really improved as well.
Summary
React is an incredible framework for creating quick, responsive and optimized user interfaces. It provides a flexible API for handling both simple and complex states, and the latest versions have improved the developer experience in such ways that most state management libraries really aren’t needed anymore.
There are definitely use cases where a separate state management layer is a net positive, and you should always consider introducing one when it’s needed. My argument is that you shouldn’t start out with one before you feel the pain of not having one. Only then can you be sure you’re not adding complexity to your stack without reaping any of the benefits.
Top comments (38)
Nitpick: one 's' in my last name, not two :)
FWIW, I've written multiple posts on this topic that cover various aspects, from "context vs Redux" to "when does it make sense to use Redux?":
I also recently put together a rough estimate of React state management library "market share". Summary:
Finally, I strongly recommend reading through the newly rewritten official tutorials in the Redux docs, which have been specifically designed to teach you how Redux works and show our recommended practices:
The older patterns shown in almost all other tutorials on the internet are still valid, but not how we recommend writing Redux code today.
It's also worth reading through the Redux "Style Guide" docs page, which explains our recommended patterns and best practices. Following those will result in better and more maintainable Redux apps.
Sorry about your name Mark!
As I mention in the article - Redux today is a much better experience than ir was only a few years ago 😊 you’ve done a great job maintaining a very important peoject that’s important for hundreds of thousands of projects across the globe.
That being said, and as I state in the article, Redux is used in many situations where it’s not needed - at least not anymore.
Big fan of your work Mark. I've been following you and your work for quite some time. I recently wrote an article on the Redux toolkit with a use-case. cloudnweb.dev/2021/02/modern-react...
Not every day you have to correct a typo of your own name in a tech article.
hah, usually it's someone trying to put a 'c' in there :) Lots of "Erickson"s in the world, not nearly as many "Erikson"s :)
I've met very few "Erik"s and NEVER an "Erick", and yet people try to spell my first name that way all the time. ;)
Editor's Note: The name's been fixed, sorry about that.
Hmmmm xstate at 8%, what do you consider it's role to be and should it be used more?
I haven't used it myself, so I can only offer a sort of generic suggestion based on seeing it described.
XState is primarily about "state machines" - having a known set of values for a given set of data, and transitioning between those in predefined possible ways based on events that occur in the system. So, it's the combination of "if in state X, and event Y comes in, transition to state Z".
It's very likely that more apps should be using something like XState for logic that is sort of an ad-hoc or implicit state machine.
As David Khourshid has pointed out numerous times, Redux reducers can be used to write state machines, but most commonly aren't, because the focus is more on the actions / event types rather than "if we're in state X, only handle these cases".
I really like XState, but I don't have much experience of building large apps so don't know practically if it scales in a usable way. My main problem is the state of mind transition from thinking about actions to thinking about states - they aren't quite the same thing. But if you make the switch life somehow seems better!
It's essentially just splitting the thought process down the middle. Our user story might say, "when I click the X button, then the dialog closes". Thus, we start thinking about the implementation in those terms as well. But instead, break it up into:
"when I click the X button, it sets the
opened
state tofalse
", then without any care for how the state got there, "whenstate.opened
is true, display the dialog".Once you not only make that transition, but start to do it reflexively, it'll really have you reevaluating how you think about software in general. ALL software is really just a state machine.
A database is a "store", with memoizing
SELECT
ors (pun intended), andINSERT
is just an action withtype: 'table_name/insert', payload: values
.REST APIs have essentially the same CRUD operations as databases.
At its core, React is just an abstraction around the subscriber model that notifies your components when a value changes. Other subscriber models include Promises, AWS lambda functions, and file watchers. ;-)
Don't use Redux in 2021. Check out React's Context API and react-query.
Great post. I love to use redux, in most cases is very helpful having your aplicacion state isolated from your components. Is true that needs more boilerplate but it worth it. I have faced issues when I need to mutate a state variable too often, and Redux was my day savior in the end. Also Redux dev tools are amazing to keep trace of your state and debug so I'll keep using Redux over Context API.
Thanks!
Those are fair points - and if the tradeoff is worth it to you, then Redux is a great choice for your projects. My argument is that many people have seen it as the default choice for any app - no matter what requirements your app might have. It shouldn’t be.
This "default choice" problem has actually eaten far too deep than we are taking about it. It's the reason why devs reach for a framework even for as little as a "coming soon" page in the first place. And this poor thinking just makes it look like frameworks are a thing for the less informed.
It's also funny how we only ADMIT the downsides of something after seeing something else. I remember talking every now and then about state management complexity without anyone admitting it. Hopefully soon, we can begin to question the entire idea of much of the frameworks themselves and admit the overkill.
I find it the most-worthwhile thing to push for native platform implementation of the same paradigms on which UI frameworks are based: modularity, reusability, and reactivity. I can as well expect most devs to only later admit to the current state of complexity when browsers finally ship with proposals like OOHTML - .github.com/webqit/oohtml.
Your examples of context contain a problem which inflates the relative performance benefit of Redux. With Redux, the higher order component flavor has a
mapStateToProps
function which extracted the props you were interested in and returned an object of props.An important feature of Redux was that this object was shallowly compared to the object returned the last time it ran. If the contents of the resulting object were the same, it didn't rerender your
connect
ed component. pseudocode:(The modern API use the
useSelector
hook, but it has similar optimizations, just on a per-value basis)Now let's compare that to the Context example above.
When it renders, it passes the object supplied to
value
down to any consumers later in the tree. It also has an optimization so that, if the value ofvalue
is the same, it doesn't trigger the consumers to rerender.BUT, there is a problem. The
value
of aProvider
doesn't have to be an object; it can be astring
,null
,42
, whatever. Thus, it only compares the top level value, not its contents. Every time theProvider
rerenders, it creates a NEW object (albeit with the same contents), so every time all of its consumers rerender. In effect (also pseudocode):The solution: ALWAYS use
useMemo
when passing an object as thevalue
of aProvider
. You might often hear not to overuseuseMemo
. It's often a premature optimization, and since it carries some overhead, it can potentially DEGRADE performance rather than enhancing it. But your customProvider
has no idea what's being rendered below it, and due to Murphy's law, we must always assume that the biggest React tree the world has ever seen has a consumer of your context at the top of it (and with something likelocale
, it probably IS at the top of yourApp
!). So the provider becomes:I actually still love Redux and despite it being big I haven't found a "big" reason to just go reach out for something different. While we do use React context we ran into some massive performance issues on a context provider that became big and was serving too many different form-related components. We managed to solve around 80% of the re-rendering that was happening within this just form simply by moving the context to Redux and creating a somewhat
ConnectedForm
, and making good use of reselect.In most cases I'd normally be using Redux, I find React's useReducer in combination with the Context API can solve the problem already. The only downside is you lose the handy DevTools integrations with frameworks such as Redux, MobX, Overmind.
I am now using mobx on production level react native app. I am using the opinionated version, mobx-state-tree, the feeling is quite similar to redux but with much less code, not touch the full mobx yet, and the fear of adding too many codes when you want to create a new store on redux is gone.
I decide to pick this after found out that Infinite Red team choose this as their primary for their RN-boilerplate, Ignite, arguably the best boilerplate for RN. Now I am not sure if I will use Redux again in the future.
Nice article. Overmind seems cool, first time I heard of it.
I think you should put more emphasis on "Redux toolkit", since it is very powerful and has a different development experience than vanilla Redux
I wonder why modals should be a good example for that, being kind of "global" in their very nature. Just imagine having two components with custom pop-overs, contained in a third component. If you only want to allow one pop-over to be open at any time, your third component would need to include handler-logic, although it's a separate concern.
I agree that the developer experience for state management could be better though, and hooks really help in that direction, independent of the library.
What do you mean it doesn't support arrays? Are you following the patterns stated in the docs? It doesn't do normalization as most users don't actually need a normalized cache. It works with gql: react-query.tanstack.com/examples/....