Skip to content

Instantly share code, notes, and snippets.

@CptLemming
Last active September 18, 2020 16:17
Show Gist options
  • Save CptLemming/d9e65e38b1e2091ae4dd to your computer and use it in GitHub Desktop.
Save CptLemming/d9e65e38b1e2091ae4dd to your computer and use it in GitHub Desktop.
Undo command pattern in Redux
import { createStore, combineReducers, applyMiddleware } from 'redux';
// Actions
const RECEIVE_UPDATE = 'RECEIVE_UPDATE';
function receiveUpdate(counter) {
return {
type: RECEIVE_UPDATE,
payload: {
counter
}
};
}
const UNDO = 'UNDO';
function undo() {
return {
type: UNDO
}
}
function add(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter + value;
return new Promise((resolve, reject) => {
resolve(newValue);
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
function sub(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter - value;
return new Promise((resolve, reject) => {
resolve(newValue);
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
function mul(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter * value;
return new Promise((resolve, reject) => {
resolve(newValue);
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
function div(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter / value;
return new Promise((resolve, reject) => {
resolve(newValue);
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
// Commands
class Command {
execute() {
throw new Error('Not Implemented');
}
undo() {
throw new Error('Not Implemented');
}
}
class AddCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return add(this.value);
}
undo() {
return sub(this.value);
}
}
class SubCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return sub(this.value);
}
undo() {
return add(this.value);
}
}
class MulCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return mul(this.value);
}
undo() {
return div(this.value);
}
}
class DivCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return div(this.value);
}
undo() {
return mul(this.value);
}
}
// Middleware
let commands = [];
function undoMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
if (action instanceof Command) {
// Call the command
const promise = action.execute(action.value);
commands.push(action);
return promise(dispatch, getState);
} else {
if (action.type === UNDO) {
const command = commands.pop();
const promise = command.undo(command.value);
return promise(dispatch, getState);
} else {
return next(action);
}
}
};
};
}
// Reducer
function counterReducer(state=0, action) {
switch (action.type) {
case RECEIVE_UPDATE:
return action.payload.counter;
default:
return state;
}
}
const appReducer = combineReducers({
counter: counterReducer
});
// Store
const createStoreWithMiddleware = applyMiddleware(
undoMiddleware,
)(createStore);
// App
const store = createStoreWithMiddleware(appReducer);
store.subscribe(() => {
let state = store.getState();
console.log('-----------------------------------');
console.log('state', state);
console.log('commands', commands);
console.log('');
});
store.dispatch(new AddCommand(10))
.then(() => store.dispatch(new SubCommand(2)))
.then(() => store.dispatch(new AddCommand(5)))
.then(() => store.dispatch(undo()))
.then(() => store.dispatch(undo()))
.then(() => store.dispatch(new MulCommand(4)))
.then(() => store.dispatch(new DivCommand(2)))
.then(() => store.dispatch(undo()))

Undo command pattern in Redux

This is my attempt at at implementing undo (no redo for now) in Redux.

Middleware is used to implement a command pattern approach to undo / redo, where incoming actions are identified as Commands and added to a stack.

When the undo() action is raised the middleware halts the current action instead calling the undo method of the previous Command.

Pitfalls

  • Due to implementing via middleware, only one stack may exist for the entire application.
  • Creating a Command to call actions which in turn return promises seems very convuluted.
  • Commands are added to the stack before the action completes. What happens for errors?
  • As commands are not in state, cannot add is_undoable functionality.
  • How to implement optimistic updates?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment