Redux in light of new React features

The popular state container Redux provides several benefits for a frontend application including sharing data between separated components, ensuring data consistency, as well as providing a template for code organisation. However, recent additions to React’s core API could suggest it might be worth re-considering, whether or not Redux is the right fit for your app.

A shift in thinking has begun to appear due to recent API changes, resulting in more than two thirds of Redux’s functionality now being found within React core itself. Alongside this, one can argue that the missing pieces provide more flexibility when maintained as ‘separate’ entities as opposed to within a large extra dependency.

Some have examined the basic idea of using the new Hooks and Context APIs for state management but what I would like to explore here is how breaking up Redux into it’s composite parts and then replacing the ones that React does not provide actually enables us to create more flexible architectures than pure Redux and without some of its implicit pitfalls.

Exploring Redux and some tradeoffs

Redux is a functional immutable state management container that is controlled using Action objects which effectively act like Events. It allows an application to use these Events to represent updates to a global immutable data object that is propagated to state change listeners. One of the defining and effective features of Redux is the way the programmer uses a Reducer function (think Array.prototype.reduce ) to merge any given Action object with the store’s current State.

Nevertheless using Redux has both benefits and tradeoffs. Let’s explore some of the pros and cons using Redux within a React application.

There are many benefits to using Redux.

It allows developers to easily share state data between different components throughout an application via the use of helper libs such as react-redux .

. You get control flow, a middleware pattern and the resulting ecosystem of middleware extensions available.

By using a single master store you can be sure your state remains consistent which has been an issue prior to Redux and Flux with certain MVC patterns.

Subsequently centralisation allows for developers to potentially explore and debug application state over time a technique supported by Redux Devtools also known as time travel debugging.

The state reducer pattern is an effective and simple way to manage and configure atomic state changes and importantly Redux provides a common language and conventions for managing state within JavaScript applications.

So that all is great however Redux is still not without some problems:

Redux tends towards becoming a global dependency that will have its tentacles throughout your codebase.

It can lead developers to be tempted to store far more state globally than is actually required.

Redux is considered as a state management container but is often used as an event bus which is a practice that depending on context could be considered an anti-pattern.

Redux does not deal well with asynchronicity out of the box and requires middleware to even support async events.

Redux does not deal with side effects out of the box either.

Integration with other implicit state management containers such as Routers have always been problematic.

You don’t actually need Redux to manage your application data because of the new React APIs.

Some of these tradeoffs can be partially addressed with splitting apart Redux to separate components and others not. At the same time we can loose some of the advantages that a standard library such as Redux provides. To look at our options let’s now examine the flow of a data change transaction to better understand what both Redux and is now providing us.

Breaking down a data transaction

Before exploring how best to use what React now brings to the table, it makes sense to take a deeper look at the ways in which Redux works conceptually so we can make meaningful distinctions with the new in-built React tools we have at our disposal.

The (state, action) => state Reducer is at the heart of Redux as it controls the transformation stage of a data transaction. This is the crucial piece of configuration that defines much of the frontend business layer but around this there are sub-systems or building-blocks that make up a chain in the sequence of a transaction. When building a Redux driven React app you might want to think about the building blocks looking a little like this:

A Component dispatches data in the form of an Action to the Pub/Sub or Event Bus implementation within Redux. The Pub/Sub sends the data through a series of Middleware that manipulate the Action proceed with a mutation or defer it. I like to call this Transaction Pre-processing Eventually the Action is sent through to a Transaction Engine which runs a pure Reducer function to merge the current State with the given Action immutably to produce a new state tree. The new state tree is stored in a Data Store which exists as an in memory object but can also be cached elsewhere such as localStorage or a database. The State Dependency Injector receives changes from the data store and provides the new state to components that require it A recipient Component finally receives the new state and re-renders displaying the updated data.

You can think about this flow and the technology mix that provides each piece in the following way where react-redux is the state dependency injector and redux itself provides several pieces. See diagram below:

Building blocks of a Redux Transaction

So what does React now bring to the table?

React’s new APIs provide facilities to replace a large chunk of the Redux components as well as all of react-redux . A comparable transaction engine can be found within its useReducer API. The useReducer hook also manages to provide an immutable data store. Then we can take advantage of the new useContext API to act as a state dependency injector. When including the client component in the count you could say that pretty much two-thirds of the whole transaction flow is now managed within features bundled with React. That’s a huge amount to offload!

Substituting React for Redux

Now above you can see that what is missing is a Pub/Sub (or Event System) and a Transaction Pre-processor.

Some might argue that useReducer provides an Event System already in that it exposes a dispatch function that can be shared to children. The thing is that because of the details of React’s Fibre re-write and eventually-consistent state resolution there is no easy way to apply a sequential transaction-preprocessing layer to useReducer . Basically this means it is far less problematic to use dispatch as a client of an Event System and the influx into the React side of the transaction as opposed to the Event System itself. You also get an extra advantage in separating your event dispatcher from your state which becomes important when you are temporally tying app contexts together that should not mix their data.

There are also some concerns (see comments below) about createContext ’s performance in terms of being able to render very fast state updates to a large shared state redux tree. You can see evidence of this in react-redux ’s technical challenges moving to createContext as they grapple with supporting pre-existing performance problems for minor edge cases. One of the things you gain by separating your event bus out from your state management is the ability to treat each part of state differently in terms of it’s performance requirements. So for most situations using React’s createContext will be fine but when you require faster update performance one technique available to you is to simply listen directly to dispatched events from the event bus and manage this internally with a local cache if required.

You may be forgiven for thinking that not avoiding Redux means you have to loose access to great tooling workflow such as the popular Redux Devtools project, however, with projects such as reinspect allowing developers to hook up Devtools to useReducer and useState, there is now no hard dependency on Redux for Redux devtools anymore.

Adding an Event System

So what should you actually use as your event system? Well, pretty much anything you like. You might consider something like RxJS because you get a transaction pre-processing engine for free. This might be the right move if you were migrating an app off redux-observable lets say.

Using RxJS as an Event Bus and Transaction Preprocessing system

Alternatively, we could use the isomorphic port of Node’s EventEmitter module to dispatch events. This will work but it doesn’t support listening to wild-carded or name-spaced events which can be useful when we want to separate out sections of our app to only respond to relevant events. I have found success with EventEmitter2 that allows for wildcard events. It may not be for everyone but my technique was to wrap this in a simple Event Bus API that contains good TypeScript support providing only the small subset of the features I actually needed in an Event Bus. I have created my own library for this technique called ts-bus .

Using an event bus only

Either of these arrangements provides more flexibility than using Redux alone.

Some benefits of a separate Event System

There are many benefits to separating out the Event System from state management.

You don’t always need state to change based on an event

Whilst it is common for state to be changed in response to an event you don’t always need to connect state change with an event. Sometimes all that is needed is to start an asynchronous background process and have it report when it is done. Bringing external state in where it is not required leads to added complexity and overhead.

You have limited your dependency exposure to a small simple component

An eventing system will become a major dependency hazard for any large app as every component that has to communicate with another will need to have access to an instance of your event dispatcher. By using a separate Event System we have avoided automatically connected state management to all your app components.

You can manage asynchronicity however you like

Events are no longer tied to state change so async becomes easier. Want to use an async function to manage asynchronous events? Easy. You control how asynchronicity is managed and what state dependencies you have available. Admittedly, implementing a cancellable saga workflow is more difficult but that is never an easy problem to solve to begin with and is rarely actually required.

It’s simple to track state in a separate store such as a Router

Occasionally state needs to be managed but cannot be tracked in your state container. This most often occurs with navigation where state is held within the browser URL via some kind of Router. Having a separate Event System means that you can easily provide a point of abstraction for your Router. An added benefit to this technique is that you don’t need to share your router code all over your application.

You have the flexibility of an event driven architecture

By using a simple Event Emitter you can handle your events however you like. Whether that be by running handlers in service workers or by setting up Observable streams or offloading computation to a server. Send events to the server, receive events from the server, synchronise a second micro-Vue frontend application, Manage side effects with RxJS; So long as you can share your event bus, your application architecture can support it. This is the power of an event based architecture you have the ability to connect up your app however you like.

As an example here we can see how conceptually you might share an Event Bus to communicate between various frontend frameworks. This might be a good approach for a huge organisation that has several teams working on their frontend stack or for teams slowly re-writing a system piece by piece:

Example micro-frontend communication over an event bus. Notice that each app manages it’s own state to avoid dependencies between codebases

Alternatively you can setup your event bus with a bridging system over something like socket.io for example to create a multiplex messaging channel between the browser and say an industrial event streaming platform such as Kafka in order to feed a large Event Sourced application. This might be a good approach for something like a financial exchange that needs realtime data.

Possible example of synchronising events between client and server.

Lastly, because we are talking about basic Event Bus architectures and Redux contains an Event Bus, these systems can certainly be set up by using Redux itself. The caveat here is that with Redux you drag along data with your bus that may actually cause more harm than good as developers inadvertently share state between systems that should remain separate.

Conclusion

Redux is a valuable and versatile library but it is more than just a state management container. When you choose to use Redux you are choosing your Event Bus, your Data Store, your canonical DI mechanism and so on and depending on a single package for all of these separate application functions may not be what you need in every situation.

Whilst it certainly makes no sense to re-write a large app to remove Redux, due to Redux’s tendency towards becoming a global dependency, new or smaller apps should think carefully about whether or not their needs can be served by selecting an appropriate event bus and using in-built React state management as this will lead to a cleaner and more flexible architecture.