Building a React state management library with built in effects support

Erik Davtyan
ITNEXT
Published in
5 min readOct 4, 2021

--

While building a chat business logic library for react, I realized that the current state management solutions are lacking. Chat is a real-time complex app with many listeners — you have to make sure to have a callback when I join a new chat room, make sure to listen to new messages and read indicators in every room, make sure to listen when a participant changes their profile picture. When people talk about state management libraries, they always talk about how it is non-trivial to handle fetch and other Promise-based async operations in them. But it is significantly harder to handle event-based asyncronosities, and to clean up after yourself not to leave any memory leaks. The best solution we have right now is redux-observables, which is a very elegant way to solve these issues, but I believe it can be done in a simpler way. Facing this issue I decided to try to build my own state management solution, starting from scratch and solving my problems as I go.

You can find the finished library here — https://www.npmjs.com/package/@plancky/state.

Firstly, I made a class that stores data and is able to communicate with the view layer when it changes.

Notifier class

Now any class that extends from it can have internal data, methods to modify it, and call this.notify() whenever the view layer should be made aware of the changes. We can then subscribe to it from the view layer and rerender when notify is called, like so.

Here is an example usage of the notifier class.

It is cumbersome to remember to call this.notify() every time a change occurs, so I decided to make a more idiomatic way of updating it — a StatefulNotifier.

This class not only informs the view layer when data changes, but implements a setState function similar to React’s setState, which calls notifiy() internally. Now we can extend from StatefulNotifier, pass our initial state and just use setState and not worry about remembering to call notify on every change.

We can wrap the creation of a StatefulNotifier into a function createStore() to make it easier to work with. It will take the initial state as the first argument, and a function to create the actions as a second argument. I took inspiration for this decision from the Zustand state management library. At this point we have an api similar in essence to Zustand.

And we can also create a simple useStore(store, selector) hook to use it in react and avoid unnecessary rerenders.

Now we can create a store like so:

Now, in our react layer, we can simply call

And it takes care of rerenders when rooms changes automatically. We can also use any of the actions from our view layer.

Now we have a full-fledged state management solution for React. However, it is still cumbersome to handle events. Let’s say we want to add an event listener for new messages every time we call actions.addRoom(). Sure, we can start the effect in the actiona and have the action return an unsubscribe. But if we ever need to remove that room, we have to make sure we have a reference to that unsubscribe function. Also, when we call actions.removeRoom() we have to also remember to call that unsubscribe to avoid memory leaks. This is not reactive and bugs will certainly arise. Ideally — we want our state manager to handle this for us. We want it to add the event listener in addRoom() and remove it in removeRoom().

Effect is a function that starts some kind of process (e.g. event listener) and returns a cleanup. A great example of this is React’s useEffect hook.

Being faced with this issue I dediced to implement support for adding and removing effects with ids in the store itself. The implementation of EffectiveStatefulNotifier looks like this.

Now we can modify the createStore function to handle effects as well. We only need to modify the Api type like so.

The rest remains the same, except instead of new StatefulNotifier(state) we now call new EffectiveStatefulNotifier(state).

Now our state management solution has native support for effects, and we can modify addRoom and removeRoom functions like so.

In the react layer we can call actions.addRoom(‘id’) and then cleanup with actions.removeRoom(‘id’) without worrying about memory leaks — the store handles that for us :)

--

--

Building and Deploying Scalable Web Apps | Senior Platform Engineer at A.Team