Last active
September 18, 2020 16:17
-
-
Save CptLemming/d9e65e38b1e2091ae4dd to your computer and use it in GitHub Desktop.
Revisions
-
CptLemming renamed this gist
Nov 18, 2015 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
CptLemming created this gist
Nov 16, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ # Undo command pattern in Redux This is my attempt at at implementing undo (no redo for now) in [Redux](https://github.com/rackt/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? This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,207 @@ 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()))