Article summary
This is the first post in a series about adding restorable randomness to a Redux app. In it, we will cover what I mean by restorable randomness, why you might want it, and some code to generate restorable random values.
Repeatable vs. Restorable Randomness
Repeatable randomness is common in software development and usually accomplished via seeding (although JavaScript’s Math.random
doesn’t actually support it). Often seen in tests, seeded PRNGs allow developers to reproduce intermittent errors caused by the random value. If the seed for the failing test is known, the test can be rerun exactly as it failed and debugged.
I define restorable randomness as a way to expand the reproducibility benefit outside of tests into the app itself. Generating identical sequences for the same seed is helpful when testing, but it’s insufficient for debugging a running app when combined with user actions. To fully reproduce a user’s experience, you need to know the state of the PRNG itself.
seedrandom.js
First, we need a JavaScript PRNG library that can save and restore state. While there are a variety of options, I have found seedrandom.js
to be an excellent choice. Crucially, it has support for saving and restoring PRNG state.
Once we’ve added seedrandom.js to our package.json
, we can integrate it into Redux by creating a new reducer specifically for rngState
.
// 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/index.js
import { combineReducers } from 'redux';
import { rngStateReducer } from './rngStateReducer';
export const rootReducer = combineReducers({
rngState: rngStateReducer,
});
We randomly seed the PRNG so the app is fully randomized each time it loads. Depending on your situation, you could use a fixed seed instead.
Usages
Directly to component
By passing the rngState
directly to the component, you can expose randomness to your UI code. While this is the most straightforward method, it breaks the abstraction of randomness as business logic.
// src/pages/rootPage.js
import React from 'react';
import seedrandom from 'seedrandom';
const RootPage = ({ rngState }) => (
{seedrandom('', { state: rngState })()}
);
const mapStateToProps = (state) => ({
rngState: state.rngState,
});
export default connect(mapStateToProps)(RootPage);
Restored PRNG to Component
Abstracting the randomness by half a step, we can restore the PRNG in the container and hand a restored PRNG to the component.
// src/pages/rootPage.js
import React from 'react';
import seedrandom from 'seedrandom';
const RootPage = ({ rng }) => (
{rng()}
);
const mapStateToProps = (state) => ({
rng: seedrandom('', { state: state.rngState }),
});
export default connect(mapStateToProps)(RootPage);
Restore PRNG in action creator via container
Since most of your business logic (hopefully) does not live at the UI level, we continue abstracting by moving the PRNG restoration out of the component/container layer entirely to the action creator. The action needs access to the state, so we can follow Redux’s suggestion to use mergeProps
.
// src/actionCreators/randomizeValue.js
import seedrandom from 'seedrandom';
export const RANDOMIZE_VALUE = 'RANDOMIZE_VALUE';
export const randomizeValueAction = (rngState) => (
{
type: RANDOMIZE_VALUE,
randomValue: seedrandom('', { state: rngState })(),
}
);
// 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,
rngState: state.rngState,
});
const mapDispatchToProps = (dispatch) => ({
randomizeValue: (rngState) => dispatch(randomizeValue(rngState)),
});
const mergeProps = (stateProps, dispatchProps) => ({
randomValue: stateProps.randomValue,
randomizeValue: () => dispatchProps.randomizeValue(stateProps.rngState),
});
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(RootPage);
Restore PRNG in action creator via Redux Thunk
Personally, I find mergeProps
cumbersome and inelegant. Since I treat the container as part of the UI layer, I want to hide all concept of randomness from it. Instead of mergeProps
, I use Redux Thunk.
// src/actionCreators/randomizeValue.js
import seedrandom from 'seedrandom';
export const RANDOMIZE_VALUE = 'RANDOMIZE_VALUE';
export const randomizeValueAction = () => (dispatch, getState) => {
const { rngState } = getState();
const rng = seedrandom('', { state: rngState });
dispatch({
type: RANDOMIZE_VALUE,
randomValue: rng(),
});
};
// 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);
Randomize in reducer
If you prefer “fat” action creators, then the above may be sufficient. I prefer “fat” reducers, so I want to move the randomness one step further. Redux has suggestions on how to share state across reducers, which should work well.
Coming Up
In my next post, I show the method I use to make the PRNG generally accessible to all my reducers (spoiler: middleware). And I describe how the app updates the stored rngState and how the Redux state is saved and loaded.