Written by: Angelo Martins
State management for modern web applications is hard. Redux is one of the best libraries for the job, but it's not perfect.
In this post, I explain the problems we had with Redux and why we decided to build our own state management library instead of using other popular libraries like MobX, Recoil or RXJs.
Problem #1: Performance
Redux has a central store and all the state information for your app should be in it.
A central store has benefits, for instance, it is easy to see all of the current state, since everything is in the same place. That is why redux is so popular.
But if you have a large application that is growing over time, the number of Redux reducers, selectors and effects will grow dramatically.
Each action will go through all of the reducers and effects and while they are intended to be fast and skip actions they don't care about, it still requires some computation. The more you have, the slower it gets.
We can also have hundreds of “selectors” at the same time in the application. Every update to application state will trigger the chain of selectors to recalculate their values and force the react components to re-render show the new data. It can make your application perform poorly.
The selectors can be optimized to cache the latest value, but that can be hard to implement and easy to miss.
There are two other performance problems that can be difficult to solve with redux itself: large data and frequent updates.
You can have so much data that it is not viable to store in the state. In our case, it started with tables that have millions of rows.
You can have data that has frequent updates in a short period of time, combined with tons of actions that update a small part of the state, it doesn't make sense to put the data into a large central store like redux.
To fix these problems we moved those pieces out of redux into classes containing the ability to subscribe to changes.
Problem # 2: Verbose
With redux there is a lot of boilerplate code to do simple tasks. We had a lot of files with actions, reducers, effects and selectors. The business logic is spread around multiple files to get it into Redux.
Problem # 3: Async code
There is no asynchronous code with redux. You need to create effects to dispatch actions when an “async” operation starts and ends.
If you need to save some data on the server, you need one action to trigger the “save” effect, another to dispatch an action to update the state to “saving”, then you dispatch a result action.
If you have some other code that wants to call a save function and wait for the operation to complete, with Redux alone, this is not possible. The code has to subscribe to actions that are dispatched as the async operation is processed.
The biggest challenge
How do we combine data in Redux state with data that is outside of redux state?
As an example, how do we handle an insert/delete button that needs to be visible/enabled according to the redux state, but also watch for the selected cell and row data (that we have kept out of Redux state due to the size) to ensure they are editable?
In order to accomplish this, we had to look for other options, we need a way to watch and combine multiple subscriptions. 🧐
Why not other libraries?
Recoil was the best option, in terms of 3rd party libraries, and we were inspired by it. Facebook created Recoil to solve the problems we have with Redux.
However, Recoil didn't work for us for several reasons:
- It is tied to React, you can only use Recoil inside React applications. We wanted to maintain flexibility in terms of which front-end framework we used to build the application.
- It needs <RecoilRoot> at the top of the react tree, but we wanted something that did not have this requirement so that a developer could create components and place them anywhere, in any app.
- We thought the selector "get" function style was less intuitive (We prefer the reselect style from Redux).
- You need a unique string key for each piece of state (atom), and you need to be careful to not duplicate keys.
MobX also looked like a good option, however:
- MobX uses a proxy to wrap objects and detected mutations. When you look to a component using MobX, you it is difficult to determine what objects are being watched and what triggered a change.
- MobX forces you to use an Objected Oriented Programming style. Any property in an object can be mutated. It is not easy to add custom equality functions to check if a state has changed (you can have new arrays and objects instances that are different appear to be the same).
At Kinaxis, we do a mix of functional programming with OOP, from OOP we use some classes without inheritance, because sometimes a class is faster and cleaner. We focus on functional programming aspects like immutability, composition, pure functions. Focusing on OOP was not an option.
We found RxJs complex and heavy. We had used RxJs before and did not have a great experience. The code can get very complicated and hard to follow and debug. For junior developers it can be very intimidating. Some of the function chains required to observe the objects were not intuitive.
No, the solution is not duct tape! We need a proper fix; a library that can solve all our issues.
It had these requirements:
- Decentralized state: One expensive part of the state shouldn't interfere the performance of the whole app.
- It should be able to create selectors that can combine different pieces of the state and other selectors.
- Subscriptions should be handled by the framework, to automatic subscribe and unsubscribe when needed.
- It needs to be easy to use, understand and debug.
- The developer should have full control of what is going on.
- It should support Asynchronous operations.
- It should batch updates, to update the UI once on the next js cycle.
It was hard to meet all those requirements, but we did it! 🎉
On the initial implementations I couldn't believe how simple it was!
Our new state management library is not open sourced yet. I hope it will be open sourced in 2022 now that it is becoming mature, battle tested and proven to be scalable, fast and easy to use.
Here is an overall idea of how it works:
The library has 2 packages:
- A core package with pure typescript code that has two main functions: createValue and createValueSelector.
- A react package that provides the useValue and useAsyncValue hooks.
The createValue function takes the following arguments:
- initialValue - value to be wrapped.
- equalityFn - (optional) function to check if the value has changed to trigger the subscribers.
And it returns an object with:
- getValue() – function to get the current value;
- setValue(newValue) – sets a new value.
- subscribe(listener)> – add a callback to be called when the value changes and it returns an unsubscribe function.
The createValueSelector function takes the following arguments:
- values – an array of values to watch for changes.
- combiner – a function that takes all the current values and returns a new value, it was inspired on the reselect lib for redux.
- equalityFn – (optional) function to check if the value has changed to trigger the subscribers.
And it returns an object with:
- getValue() – function to get the current value;
- subscribe(listener) – add a callback to be called when the value changes and it returns an unsubscribe function.
createValueSelector has no setValue function, the internal value is private and always derived from the combiner function passed to it.
The useValue hook can be used inside react components to automatically subscribe to values and unsubscribe when the component is removed from the page.
Like the useValue hook, useAsyncValue hook can be used inside react components to automatically subscribe to “Promise” values and unsubscribe when the component is removed from the page. The hook exposes the state of a promise as well as the result or error derived from the value when it is resolved.
That is not all! This is just a quick overview of our new state management library. It has many other functionalities and tools for debugging memory leaks, detect unnecessary update and hints on how to resolve them. There will be more exciting developments to come as the library evolves.
Redux is good, but there is no one-size-fits-all library out there. Evaluate the options and find out what works best for you and your team.
I hope you understand why we had to move away from redux, and why we decided to build our own state management library.
Angelo Martins is a senior software developer in Ottawa, Ontario and a former principal software developer at Kinaxis.