Skip to content

Instantly share code, notes, and snippets.

@CptLemming
Last active September 18, 2020 16:17
Show Gist options
  • Select an option

  • Save CptLemming/d9e65e38b1e2091ae4dd to your computer and use it in GitHub Desktop.

Select an option

Save CptLemming/d9e65e38b1e2091ae4dd to your computer and use it in GitHub Desktop.

Revisions

  1. CptLemming renamed this gist Nov 18, 2015. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. CptLemming created this gist Nov 16, 2015.
    14 changes: 14 additions & 0 deletions README.md
    Original 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?
    207 changes: 207 additions & 0 deletions remote_undo.js
    Original 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()))