Replacing Redux

This is an educational piece on state management in modern JavaScript single page applications.

I’m hardly recommending any frameworks and strongly advise against using React. However, Redux, React’s state manager is among the select few dependencies I have recommended several times, including on this blog. The reason is, it’s small yet powerful. And that’s what you should be looking for. Because the code you depend upon becomes a maintenance concern and you should maximize the bang you get for your maintenance buck.

Redux also may be the only dependency I keep using myself in a private project, because it’s just so good. Now I faced a performance problem in that app and had difficulty finding the culprit. Redux kept popping up (spoiler: Redux was innocent!) and I considered what I’d need to get rid of it.

Said application consists of 50% Redux specific code – always maximize the purely functional proportion of your code! And I wasn’t going to rewrite that. However, I looked at what parts of the Redux API my app is actually using and I thought it might be feasible to replace that by something simpler of my own devise.

I embarked on what I thought might be a few days of learning and fun programming. 15 minutes later I was done. And my app was running the same as before. I’ll first show you my Redux replacement then discuss Redux a bit more. This is not going to be Redux bashing, quite to the contrary!

Redux replacement code

export class ReduxReplacement {
	#listeners = [];
	#reducer;
	#store = {};

	constructor(topLevelReducer) { this.#reducer = topLevelReducer; }

	getState() { return this.#store; }

	dispatch(action) {
		this.#store = this.#reducer(this.#store, action);
		deepFreeze(this.#store);
		for(const listener of this.#listeners) listener();
	}

	subscribe(listener) { this.#listeners.push(listener); }
}

function deepFreeze(object) {
	const propNames = Reflect.ownKeys(object);
	for(const name of propNames) {
		const value = object[name];
		if (
			(value && typeof value === 'object') ||
			typeof value === 'function'
		) deepFreeze(value);
	}

	return Object.freeze(object);
}

If you haven’t seen the funny #… syntax at the top: that’s private class members in JavaScript. So, that’s it, my complete Redux replacement. I think Redux clones the state when you call getState. I freeze it instead (with code taken from MDN) for performance reasons and because I know, I can get away with it, because I nowhere manipulate the state passed to me. But the cloning function would look quite similar.

I’ll probably keep using my replacement, because it saves my SPA some 17k (unminified, not zipped) code, bringing it to below a 100k (unminified, not zipped) total. But I wouldn’t even strongly recommend that. Redux is fine, and 17k is tiny in most modern SPAs.

The reason I’m sharing this is an implication that only became clear to me after this exercise: Redux is a great concept. The code is very fine, too, as far as I can tell, but it’s the concept that really shines. And it’s API is obviously quite outstanding, clean, simple and transparent – otherwise my little stunt wouldn’t have had a snowballs chance in hell. But let’s stick with the concept:

Redux abridged

Your data (the whole application’s state!) is kept in a JSON style object. When the server or user changes that state it does so by dispatching a change event to your data store. The store then calls a pure function (I recommend getting yourself familiar with functional programming if you aren’t already) to create a new version of the state.

The state is never changed, only cloned with changes made while creating the clones. That is (plus first class functions) functional programming in a nutshell. The new state creation is executed by a pure function that gets the old state and the event as input.

The whole JSONesque state usually takes the shape of a tree of nested objects and arrays. The new state creation function usually calls other pure functions to handle parts of the state tree. Each function determines itself if an event affects the part of the tree it manages. If it doesn’t affect that part, the function returns the previous version of the state.

To this end all functions are passed that previous version of their respective part of the state, the event, and the whole state. And thus change detection is as simple as old === new on any part of the state.

That’s most of it, though I guess I’ll have missed some important bit. Anyway, this Redux way of state management may feel a bit cumbersome when you get started with it. But the results are outstanding (assuming you stick to some more rules of clean code). I highly recommend it for all but the simplest applications. And I actually do consider replacing Redux to be a serious and very valuable option for any project, including professional projects.

The reason is the ownership problem. Should Redux ever become abandoned or become too closely coupled with React, then you do not need to rewrite your state management. You can simply fall back on your own tiny replacement. Just having that as an option is a great asset for the longevity of your project and no reason to ditch Redux.

The Nanny Lib

And there are, sadly, indications, that Redux’ best time is over. As the average web developer experience and competency – beyond god frameworks that take the developer’s hand and never let go – is never improving due to the continued exponential growth and turnaround of the market, Redux apparently feels compelled to also take the developer’s hand, set up their stores with Redux Toolkit, manipulate their state with Immer … you get the picture.

Add layer upon layer of abstraction onto each problem as an insurance against incompetence and bad choices. And thus bloat your software, reduce user experience and make long maintenance periods ever more tedious.

Actually, if you don’t know what your doing and don’t have someone around who does, I second that approach. But you should also learn. And with that said, Redux is a great place to learn state management. Check it out. And hopefully, in the mid term, acquire the competence to devise your own state management.

Own it

As an example, I’ll show the top level state creator of my app here:

function toplevelReducer(state = {}, action) {
	const newState = createNewState(state, action);
	newState.displayItems = displayItems(
		state.displayItems, action, newState
	);
	return newState;
}

function createNewState(state, action) {
	const reducers = [
		addingShop, filter, mode, nav, items, opened,
		selected, serverUpdates, shops, tags,
	];
	return {
		...Object.fromEntries(reducers.map(
			reducer => [
				reducer.name,
				reducer(state[reducer.name], action, state)
			],
		)),
	};
}

In many cases the createNewState function will do. Just add your “reducer” functions for different branches of the tree to the reducers array and you’re done. In my case the displayItems “reducer” needs information created by other reducers, thus it’s a two step process here.

The point is: It’s not that difficult! Find a simple elegant way to express your needs. Learn from Redux (and other libraries) instead of just using them. You and your results will profit.

Leave a Reply

Your email address will not be published. Required fields are marked *