State management is key to sophisticated modern UIs, and there are many solutions available today to address it. Without a doubt, Redux is the most famous among them — you either love it or you hate it, but you cannot ignore it.
Redux has gained impressive traction over the years, largely because of the problem it solves and how nicely it does so. Being so widely used, Redux has been commended by some and criticized by others. Much of the praise comes from developers working on large-scale apps, which Redux helps to keep scalable, predictable, and performant.
On the flip side, many of the critiques are based on widely held opinions and so-called best practices that have led developers to trouble. In my opinion, Redux — if implemented wisely — can provide a truly solid base for any large-scale application.
Here we have compiled a list of must-do practices for Redux to avoid any major pitfalls in any large-scale apps.
Use it when you need it!
The same initially happened with Redux. When it appeared, many thought there was no better way of handling state management, and many still share this opinion. Others pointed to several issues with Redux and still do so today.
So who’s right? In my opinion, there’s no simple answer to this question. It all depends upon the scale of your app, and other factors like your set of feature requirements and your team’s capabilities. The good thing about Redux, however, is that:
1. You will realize for yourself when you need Redux. If you’re still confused as to whether you need it, you don’t.
Redux allows you to manage your app’s state in a single place and keep changes in your app more predictable and traceable. It makes it easier to reason about changes occurring in your app. But all of these benefits come with tradeoffs and constraints. I would argue these constraints help you maintain apps well in the longer run.
Single store per app
The guiding principle of Redux is to centralize your app’s state. This sets the base for powerful capabilities like time traveling, state persistence, and many others. With this centralized state concept, Redux allows segregation between data domains by splitting and combining reducers. Thus, in most cases, there is no need for multiple stores in a single app.
2. Standard apps will have a single store per app.
Though it is possible to have multiple stores in a single app, that makes subscriptions, persistence, and rehydration real tough. It also makes integration of the Redux DevTools impossible. Having said that, someone may find it to be their last resort for performance or scalability purposes in really huge apps.
Immutability has been a real power-booster behind the sophisticated web apps we see today. It has made complex change detection techniques simple, resulting in computationally expensive operations occurring only when they are required.
One of the most common causes of bugs in Redux apps (or, indeed, general JS) apps is due to mutating objects directly. In our case, this may prevent re-rendering of components, break time traveling, and hamper the Redux DevTools. Immutability will also bring increased performance and better reasoning to your app’s code.
3. Avoid mutating state directly in reducers (use Immer, preferably).
Writing immutability logic can be a hassle. Immer is a 3KB gzipped library that makes it super easy by allowing you to make all changes to an intermediary draft state. It then produces the next state based on these mutations.
Manageable and serializable slices
Redux suggests to divide a centralized app’s store into smaller, more manageable chunks, called slices.
Each slice has a separate reducer function handling all mutations occurring in its state. As such, every slice will own its part of the state and manage all changes relevant to that part of the state. All of these slices are tied together via the
combineReducers API that takes in a key-value pair: the name of the slice as the key, and its reducer as the value.
4. Each slice owns its part of the state: a serializable object, named in the app reducer based on the data domain.
One may consider the store as a DB that holds data to drive the app’s state, with each slice considered to be a table holding a data domain.
Having said that, the name of each slice should represent the data domain it holds — for example, “places”, “deals”, “banks”, etc. Building on the same concept, the store must not contain any non-serializable values, as that will break persistence, rehydration, and the DevTools.
Use the Ducks pattern
One of the major architectural debates surrounding large scale React-apps concerns the file structure. Though Redux is not dependent on how your files are structured, it plays an important role in the maintenance of large-scale apps. As such, many patterns have emerged in the community.
Some suggest to group reducers, actions, constants, and sagas together in respective directories, similar to patterns used in Rails. This may work for smaller apps with limited features, but as soon as the feature set grows, this pattern may become hard to maintain since specific, related files that may change together are usually stored apart from each other.
The other (and highly recommended) approach is to group files based on features. Your store will be divided into slices, and each slice’s constants, actions, reducers, and side effects are stored together in one directory. This makes it easier to develop and debug one feature at a time.
5. It’s better to group files based on features rather than file types — aka, the Ducks pattern.
This has been a battle-tested pattern and works perfectly fine for most complex React apps. The name comes from the latter half of Redux: “dux.” It makes apps more scalable and easier to debug, allowing developers to change and test each feature independently.
Use Redux Toolkit
One of the most popular and enduring critiques is the amount of code Redux adds to the app, which many feel is unnecessary. Some also suggest Redux requires too many additional packages to get it running. This criticism is usually based on opinionated best practices that are thought to be essential for Redux apps.
All of these critiques and debates eventually led to the creation of Redux ToolKit (RTK), so:
6. Use the official, opinionated, batteries-included toolset for efficient Redux development: Redux Toolkit.
RTK can truly speed up the development process while keeping code quality high and app performance intact. It comes with utility functions that help you write your slices faster by avoiding a good amount of boilerplate code, and it can be integrated into existing Redux apps without any major changes. Redux Toolkit has effectively eliminated many of the arguments raised against Redux.
Debug smartly with Redux DevTools
The finest edge that Redux has over many other state management solutions is the ecosystem that has developed around it, and the amazing DevTools are an essential part of it.
For large-scale applications that consist of multiple actions and reducers, monitoring changes occurring app-wide can be a big challenge. Redux empowers apps with the capability to undo/redo actions and to time-travel, and the Redux DevTools leverage these to make the developer’s experience joyful.
7. The Redux DevTools provide one of the best reasons to use Redux over other state management solutions.
If leveraged to the full extent, Redux DevTools can prove to be the most essential part of the development toolset. This can make Redux app development and debugging much faster and, yes, even fun.
Design the store wisely
Some Redux users confuse the principle of global state management with keeping every single value in Redux store, totally ignoring components’ local state management.
Though there is no one right way of deciding what should be kept in store and what should be kept in a local state, there are certain parameters that can help you decide. In order to keep apps performant and more scalable, it is necessary to design Redux stores wisely.
8. Leverage local state, and avoid unnecessary values in the store. Only values used app-wide are to be placed in the store.
Every developer has to decide what value makes up the app’s state and what can be placed in the local state nearby. Some rules can be set for making these decisions easier, however: if data is used to derive some other data, if it has been used in any other part of the app, or if that data is cached or hydrated, it should be placed in the store.
Wrapping it up
React is so good that it is entirely possible to write a complete app using bare React. As your app starts growing into a beast, however, Redux can be a savior. It can keep apps more predictable, testable, and scalable.
But as with any other technology, there are tradeoffs involved in using Redux for state management. Even with all of these tradeoffs, in my opinion, Redux is one of the top solutions — if not the single best — for state management in the frontend world today.
LogRocket: Full visibility into your web apps
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.