I have mixed feelings about Redux. I really like the general ideas behind Redux, and how nice they’ve explained how and when to use it. I do not like the implementation however. The main reason for that, is that they invented their own vocabulary around existing design patterns. This distracts you from the patterns they use and that in turn complicates learning.
Why would one call something a “reducer” when it is in fact a “mediator”. Why would you call something an “action” when it is a “command”? The most important Redux framework method is called “dispatch” while it has nothing to do with events. As it invokes a command on the state, it should have been called “invoke” or “invokeCommand”. If they had done a better job on that it would have been clear that we are talking about a basic implementation of the Command pattern.
Another thing I struggle with is that they pass strings around to define which actual commands to execute. Inside the “dispatch” method they call the “reducer” which uses a switch statement to finally invoke the actual required command.
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
This is very difficult to maintain and almost impossible to refactor. The only good part about it, is that you can easily serialize and store the list of executed “commands” and then deserialize and replay that list on the initial state, to get to the current state. How useful that is, depends on your application. I do think it is at least a bit smelly.
Eventually I decided to create my own predictable state container using TypeScript and standard design patterns.
My own predictable state container
Let’s start with the core.ts:
namespace Core {
"use strict";
export interface ICommand<S> {
execute(state: S): S;
undo(state: S): S;
}
export interface IInvoker<S> {
invoke(command: ICommand<S>): void;
}
export interface IRecorder {
canUndo: boolean;
undo(): void;
canRedo: boolean;
redo(): void;
}
export interface IObservable {
subscribe(callback: () => void): number;
unsubscribe(id: number): void;
}
export interface IContext<S> {
state: S;
}
interface ISubscription {
id: number;
callback: () => void;
}
export class Context<S> implements IContext<S>, IInvoker<S>, IObservable {
private _state: S;
constructor(initialState: S) {
this._state = initialState;
}
public get state(): S {
return this._state;
}
protected setState(state: S): void {
this._state = state;
this.notify();
}
public invoke(command: ICommand<S>): void {
const state: S = command.execute(this.state);
this.setState(state);
}
private _subscriptionId = 0;
private _subscriptions: ISubscription[] = [];
public subscribe(callback: () => void): number {
const id: number = ++this._subscriptionId;
this._subscriptions.push({ id: id, callback: callback });
return id;
}
public unsubscribe(id: number): void {
this._subscriptions = this._subscriptions.filter((value: ISubscription) => {
return value.id !== id;
});
}
private notify(): void {
this._subscriptions.forEach((value: ISubscription) => {
value.callback();
});
}
}
export class RecordingContext<S> extends Context<S> implements IRecorder {
private _commands: ICommand<S>[] = [];
private _current: number = -1;
constructor(initialState: S) {
super(initialState);
}
public invoke(command: ICommand<S>): void {
super.invoke(command);
this._commands.splice(this._current + 1);
this._current = this._commands.push(command) - 1;
}
public get canUndo(): boolean {
return this._current >= 0;
}
public undo(): void {
if (this.canUndo) {
const command: ICommand<S> = this._commands[this._current--];
const state: S = command.undo(this.state);
this.setState(state);
}
}
public get canRedo(): boolean {
return this._current < this._commands.length - 1;
}
public redo(): void {
if (this.canRedo) {
const command: ICommand<S> = this._commands[++this._current];
const state: S = command.execute(this.state);
this.setState(state);
}
}
}
}
The core consists of no more than a few interfaces and two generic classes. One class that maintains a list of executed commands, and one that does not. Why I like my implementation better is because I use the same commonly known design patterns, but with the standard vocabulary. Besides that my commands are also passed strongly typed. Which makes it refactorable and maintainable, and prevents you from making typo’s. The drawback is that they are not serializable anymore. Up to you to decide here. Compiled and minified to ECMAScript 6, the whole core takes up no more than 1kb.
Let’s have a look on how to use this core library when we create React.Components in a UI.tsx:
namespace UI {
"use strict";
export interface ICounterProps<S> {
context: Core.Context<S>;
getValue: (context: Core.Context<S>) => any;
incrementCommand: Core.ICommand<S>;
decrementCommand: Core.ICommand<S>;
}
export interface ICounterState {
value: number;
}
export class Counter<S> extends React.Component<ICounterProps<S>, ICounterState> {
private _subscription: number;
constructor(props: ICounterProps<S>) {
super(props);
this.state = {
value: this.props.getValue(this.props.context)
};
}
public componentDidMount(): void {
this._subscription = this.props.context.subscribe(() => {
this.setState({
value: this.props.getValue(this.props.context)
});
});
}
public componentWillUnmount(): void {
this.props.context.unsubscribe(this._subscription);
}
public render(): JSX.Element {
const buttonStyle: React.CSSProperties = { minWidth: "50px" };
const {context, incrementCommand, decrementCommand} = this.props;
const {value} = this.state;
return (<div>
Value: {value}
<div>
<button
style={buttonStyle}
onClick={() => context.invoke(incrementCommand)}>
+
</button>
<button
style={buttonStyle}
onClick={() => context.invoke(decrementCommand)}>
-
</button>
</div>
</div>);
}
}
export interface IUndoRedoProps {
context: Core.IRecorder & Core.IObservable;
}
export class UndoRedo extends React.Component<IUndoRedoProps, {}> {
private _subscription: number;
constructor(props: IUndoRedoProps) {
super(props);
}
public componentDidMount(): void {
this._subscription = this.props.context.subscribe(() => {
this.forceUpdate();
});
}
public componentWillUnmount(): void {
this.props.context.unsubscribe(this._subscription);
}
public render(): JSX.Element {
const buttonStyle: React.CSSProperties = { minWidth: "50px" };
const {context } = this.props;
return (<div>
<button
style={buttonStyle}
disabled={!context.canUndo}
onClick={() => context.undo()}>
undo
</button>
<button
style={buttonStyle}
disabled={!context.canRedo}
onClick={() => context.redo()}>
redo
</button>
</div>);
}
}
}
The important part here is that we pass Core.ICommand<S> for binding to events instead of strings.
So let’s have a look at the App.tsx
namespace App {
"use strict";
const update: (value: any, spec: React.UpdateSpec) => any = React.addons.update;
interface IState {
number: number;
char: string;
}
class Context extends Core.RecordingContext<IState> {
constructor() {
super({ number: 0, char: "a" });
}
}
const Commands = {
IncrementNumber: {
execute: (state: IState): IState =>
update(state, { number: { $set: state.number + 1 } }),
undo: (state: IState): IState =>
update(state, { number: { $set: state.number - 1 } })
},
DecrementNumber: {
execute: (state: IState): IState =>
update(state, { number: { $set: state.number - 1 } }),
undo: (state: IState): IState =>
update(state, { number: { $set: state.number + 1 } })
},
IncrementChar: {
execute: (state: IState): IState =>
update(state, { char: { $set: String.fromCharCode(state.char.charCodeAt(0) + 1) } }),
undo: (state: IState): IState =>
update(state, { char: { $set: String.fromCharCode(state.char.charCodeAt(0) - 1) } })
},
DecrementChar: {
execute: (state: IState): IState =>
update(state, { char: { $set: String.fromCharCode(state.char.charCodeAt(0) - 1) } }),
undo: (state: IState): IState =>
update(state, { char: { $set: String.fromCharCode(state.char.charCodeAt(0) + 1) } })
}
};
export function init(): void {
const root: HTMLElement = document.getElementById("root");
const context: Context = new Context();
type Counter = new () => UI.Counter<IState>;
const Counter: Counter = UI.Counter as Counter;
ReactDOM.render(<div>
<Counter
context={context}
getValue={(context: Context) => context.state.number}
incrementCommand={Commands.IncrementNumber}
decrementCommand={Commands.DecrementNumber} />
<Counter
context={context}
getValue={(context: Context) => context.state.char}
incrementCommand={Commands.IncrementChar}
decrementCommand={Commands.DecrementChar} />
<hr />
<UI.UndoRedo
context={context} />
</div>
, root
);
};
}
window.addEventListener("load", (ev: Event) => {
App.init();
});
There are a lot of way’s to define commands now. In the above example I’ve created a const Commands object that contains the command objects as I do not need any per command state or properties. You could of course do something like this to define a custom command just as well:
class CustomIncrementCommand implements Core.ICommand<IState> {
private properties:ICustomIncrementCommandProps;
constructor(properties: ICustomIncrementCommandProps) {
this.properties = properties;
}
public execute(state: IState): IState {
return update(state, { number: { $set: state.number + properties.incrementValue} });
}
public undo(state: IState): IState {
return update(state, { number: { $set: state.number - properties.incrementValue} });
}
}
You could then pass the constructor to your components and in the component itself, create a new instance of the command, with the properties based on the current component state.
All in all we now have a complete, generic, strongly typed predictable state container using TypeScript and standard design patterns, that also uses the proper vocabulary.
UPDATE:
That same friend asked me if he should now use my implementation of a predictable state container or the standard Redux implementation. That should be the Redux implementation of course! There are a lot of people working on maintaining Redux and there are a lot of (browser)plugins that you can use with the standard Redux implementation. The value of Redux is not just in Redux itself, but also the eco-system around it. The reason I wrote this article and a sample on how I would do it, is just because I think that if they’d at least adhered to the standard design patterns, it would have been a lot easier to learn how to use Redux.
Have fun,
Wesley