Restorable Randomness in a Redux App, Part 2 – Middleware & Reducers

Yesterday, I talked about getting started with adding restorable randomness to a Redux app. Today I’ll cover using the PRNG in “fat” reducers to isolate the presence of rngState from the majority of the code.

Randomize in Reducer

At the end of the previous post, we saw how to access the rngState in “fat” action creators, but I prefer “fat” reducers. Randomness is a cross-reducer concern, and Redux has some general suggestions on how to share state across reducers.

One of those suggestions is to reshape the state tree. This is a good thought, but I am a bit of a purist and don’t like the idea of compromising my domain model for convenience.

Another suggestion is to use Redux Thunk to pass the state down as part of the action. This works well when only a couple of actions use randomness, but the additional boilerplate becomes tedious to maintain at scale. However, the idea is good, so we’ll reduce boilerplate with middleware!

rngStateInjector

Redux middleware is a complicated topic, and we won’t dig into it here. Just know that it has access to the entire Redux store and can modify actions before being reduced. This lets us hydrate the PRNG from the rngState and inject the rng function into every single action.


// src/middlewares/rngStateInjector.js
import equal from 'fast-deep-equal';
import seedrandom from 'seedrandom';


export const rngStateInjector = store => next => action => {
    const { rngState } = store.getState();

    const rng = seedrandom('', { state: rngState });
    const result = next({ rng, ...action });

    return result;
};

Usage

An example system of reducers, action creators, and components using this middleware would look like this:


// src/reducers/rngStateReducer.js
import seedrandom from 'seedrandom';


const rng = seedrandom();

const DEFAULT_STATE = seedrandom(rng(), { state: true }).state();

export const rngStateReducer = (state = DEFAULT_STATE, action) => {
    switch (action.type) {
        default:
            return state;
    }
};

// src/reducers/randomValueReducer.js
import { RANDOMIZE_VALUE } from '../actionCreators/randomizeValue';


const DEFAULT_STATE = 0;

export const randomValueReducer = (state = DEFAULT_STATE, action) => {
    switch (action.type) {
        case RANDOMIZE_VALUE:
            return action.rng();
        default:
            return state;
    }
};

// src/reducers/index.js
import { combineReducers } from 'redux';
import { rngStateReducer } from './rngStateReducer';
import { randomValueReducer } from './randomValueReducer';


export const rootReducer = combineReducers({
    rngState: rngStateReducer,
    randomValue: randomValueReducer,
});

// src/actionCreators/randomizeValue.js
export const RANDOMIZE_VALUE = 'RANDOMIZE_VALUE';

export const randomizeValueAction = () => ({
    type: RANDOMIZE_VALUE,
});

// src/pages/rootPage.js
import React from 'react';
import { randomizeValueAction } from '../actionCreators/randomizeValue';


const RootPage = ({ randomValue, randomizeValue }) => (
    <div>
        {randomValue}
        <button onClick={randomizeValue}>Randomize Value</button>
    </div>
);

const mapStateToProps = (state) => ({
    randomValue: state.randomValue,
});

const mapDispatchToProps = {
    randomizeValue
};

export default connect(mapStateToProps, mapDispatchToProps)(RootPage);

At this point, rngState usage has been isolated to the middleware and the singular rngStateReducer; the other reducers only interact with the PRNG rng, while the container, component, and action are all oblivious to randomness in the system.

Updating rngState

Note that the code currently written never updates rngState. We can have it update at the end of each reduce by modifying the middleware and rngStateReducer. The middleware will now dispatch an additional action to update the rngState immediately after the original action finishes being reduced. By only dispatching the updateRngStateAction when it has changed, we avoid this extra dispatch for reductions that don’t use rng at all.


// src/middlewares/rngStateInjector.js
import equal from 'fast-deep-equal';
import seedrandom from 'seedrandom';
import { updateRngStateAction } from '../actionCreators/rngState';


export const rngStateInjector = store => next => action => {
    const rngStateBefore = store.getState().game.rngState;

    const rng = seedrandom('', { state: rngStateBefore });
    const result = next({ rng, ...action });

    const rngStateAfter = rng.state();
    if (!equal(rngStateBefore, rngStateAfter)) {
        next(updateRngStateAction(rngStateAfter));
    }
    return result;
};

// src/reducers/rngStateReducer.js
import seedrandom from 'seedrandom';
import { UPDATE_RNG_STATE } from '../actionCreators/rngState';


const rng = seedrandom();

const DEFAULT_STATE = seedrandom(rng(), { state: true }).state();

export const rngStateReducer = (state = DEFAULT_STATE, action) => {
    switch (action.type) {
        case UPDATE_RNG_STATE:
            return action.rngState;
        default:
            return state;
    }
};

// src/actionCreators/rngState.js
export const UPDATE_RNG_STATE = 'UPDATE_RNG_STATE';

export const updateRngStateAction = (rngState) => ({
    type: UPDATE_RNG_STATE,
    rngState,
});