Daniel Earwicker Chief Software Architect
FISCAL Technologies Ltd

Redux in Pieces


JAVASCRIPT IMMUTABILITY FUNCTIONAL REDUX 2017-01-28

Last July I noted down my thoughts on Redux with some hints of the concerns that eventually led to Immuto.

I've since rediscovered my love of observable and computed via MobX, which is like the good parts of Knockout.js made even better by a very careful, thoughtful implementation.

Even so, this is not the same thing as abandoning immutability and purity. There's nothing stopping you using those techniques within a system of observables. Indeed bidi-mobx abstracts away all mutation and allows entire UIs to be declared from pure expressions. The data transformation is carried out by objects called adaptors that contain pairs of pure functions between View and Model representations. Only the user gets to do mutation!

What is the advantage of modelling change through immutability? Primarily it's to allow the different versions of the state to co-exist. One part of your application can refer to an old version even as another is working from a new version. Sometimes this is what you want (an undo stack keeps old versions). Other times it's not (you're concerned mostly about data consistency and you want all parts of your app to be on the same page). Also it's worth remembering that old versions can be retained by cloning, so immutability is not the only way to retain history.

Consider React: every time you return a new UI structure from render, it compares it with the structure you returned last time and so is able to mutate the DOM accordingly. Imagine flipping this around. In your code, right after you mutate your data structure, you ask a library to take a snapshot. The library walks your current data to create a deep clone of it, compares it with the clone it made last time, and saves a succinct statement of the differences in its history stack. This, after all, is what a log of actions is: an ordered sequence of instructions for how to mutate the data in little steps. So it's like the library is watching you mutate your data and automatically recording a description of each mutation (at snapshot boundaries), which allows you to visit all historic states (i.e. "undo") whenever you need to construct them. It's more expensive at runtime, but it may also be much easier to write your app, and remember Jackson's Rules of Optimisation:

  1. Don't.
  2. (for experts) Don't yet.

My point is that immutability is just a tool for achieving something, and there are other ways. It depends what you want to make easy or fast. This varies depending on the application.

But anyway, (partly) as a joke I wrote down a minimal Redux implemented over MobX so I wouldn't need a subscribe method:

import { observable, runInAction } from "mobx";

// An action has a string property called type
export interface Action<Type extends string> { readonly type: Type; }

// Reducer evolves state as instructed by an action
export type Reducer<State, Action> = (state: State, action: Action) => State;

export interface Store<State, Action> {
    getState(): State;
    dispatch(action: Action): void;
}

// Stores a State value and uses reducer that accepts Action (typically a union of Action<T> variants)
export function createStore<State, Action>(init: State, reducer: Reducer<State, Action>) {
    const state = observable.shallowBox<State>(init);
    return {
        getState(): State {
            return state.get();
        },
        dispatch(action: Action) {
            runInAction(() => state.set(reducer(state.get(), action)));
        }
    }
}

It's worth asking: what's the advantage of actions being "pure data" (i.e. JSON-persistent)? Obviously its that they can be persisted and shipped elsewhere to be replayed. If you don't need that (and I would hazard a guess that almost no apps using Redux rely on this capability at all) then you could toss it out.

An action could be a function:

// An action evolves state:
export type Action<State> = (previousState: State) => State;

Given a state, it returns a new state. It can do this however it wants. Crucially it's not constrained at all so there is no enforcement of an invariant on the state. No problem:

// An invariant checks that a state is allowed:
export type Invariant<State> = (possibleState: State) => boolean;

So a store is just:

export interface Store<State> {
    getState(): State;
    dispatch<A extends Action<State>>(action: A): void;
}

export function createStore<State>(init: State, invariant: Invariant<State>) {

    const state = observable.shallowBox<State>(init);

    return {
        getState(): State {
            return state.get();
        },

        dispatch<A extends Action<State>>(action: A) {
            runInAction(() => {
                const possible = action(state.get());
                if (!invariant(possible)) {
                    throw new Error();
                }
                state.set(possible);
            });
        }
    }
}

So now the store is responsible for protecting the invariant. It doesn't allow changes that break the invariant. Only valid states are allowed. But you can dispatch actions to get to allowed states by whatever route you want.

After all, what operations are available on some state is not the issue. The issue is understanding what the invariant is, and enforcing it rigidly. The Redux action/reducer pattern does not capture this or enforce it. It's up to the author of the reducer to impose consistency on themselves. Redux focuses on interpreting actions into updates on state, only because it introduces that problem by insisting that actions are pure JSON-able data (just in case that's useful).

Summary: think about what you actually need, and use the abstractions that aid you in achieving it.

Time reversible events 2023-04-07
Language Smackdown: Java vs. C# 2023-03-07
Domesday '86 Reloaded (Reloaded) 2021-02-07
The Blob Lottery 2020-09-27
Abstraction is a Thing 2020-03-07
Unfortunate Bifurcations 2019-11-24
Two Cheers for SQL 2019-08-26
Factory Injection in C# 2019-07-02
Hangfire - A Tale of Several Queues 2019-05-24
How Does Auth work? 2018-11-24
From Ember to React, Part 2: Baby, Bathwater, Routing, etc. 2018-03-18
From Ember to React, Part 1: Why Not Ember? 2017-11-07
json-mobx - Like React, but for Data (Part 2) 2017-02-15
Redux in Pieces 2017-01-28
Box 'em! - Property references for TypeScript 2017-01-11
TypeScript - What's up with this? 2017-01-01
MobX - Like React, but for Data 2016-12-28
Eventless - XAML Flavoured 2016-12-24
Immuto - Epilogue 2016-12-20
Immuto - Radical Unification 2016-09-22
Immuto - Working with React (An Example) 2016-09-16
Immuto - Strongly Typed Redux Composition 2016-09-11
TypeScript - What is a class? 2016-09-11
TypeScript and runtime typing - EPISODE II 2016-09-10
TypeScript and runtime typing 2016-09-04
What's good about Redux 2016-07-24
TypeScript multicast functions 2016-03-13
Introducing doop 2016-03-08
TypeScript is not really a superset of JavaScript and that is a Good Thing 2015-07-11
A new kind of managed lvalue pointer 2014-04-27
Using pointer syntax as a shorthand for IEnumerable 2014-04-26
Adding crazily powerful operator overloading to C# 6 2014-04-23
Introducing Carota 2013-11-04
Want to comment on anything? Create an issue!