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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
Install LogRocket via npm or script tag.
LogRocket.init() must be called client-side, not
Vite is a versatile, fast, lightweight build tool with an exceptional DX. Let’s explore when and why you should adopt Vite in your projects.
Explore advanced capabilities for content sharing with the
navigator.share API, including dynamic content sharing, custom share targets, and batch sharing.
We spoke with Chas to get his insights on building technology and internal processes for companies that are scaling quickly.
Cypress is one of today’s foremost tools for testing web applications. Let’s explore when and why you should adopt Cypress in your projects.