Easy-peasy: Undo / Redo, Howto?

Created on 9 Aug 2020  路  9Comments  路  Source: ctrlplusb/easy-peasy

Any suggestions on how to implement undo / redo as an easy-peasy user?

I see some discussion in #124

docs

Most helpful comment

I've published a library containing an undo/redo implementation for easy peasy apps. It's here: patched-undo-peasy.

It uses the state history suggested by @GavinRay97 and redux middleware, as suggested by @jchamb. Thanks for helping out.

The current implementation of undo/redo does not depend on the postActionReducer patch above. However, my app requires a patched version of easy peasy due to #539, so the implementation depends on patched peasy.

All 9 comments

Sure, here's a pretty gnarly, rough-around-the-edges (mostly) working solution I tried putting together for this. Breaks when you try to "undo" but you only have one forward state change in history (probably because the history object is initialized with present as null instead of taking an init object argument and using that).

https://codesandbox.io/s/interesting-microservice-50jeh?file=/src/store.ts
https://codesandbox.io/s/interesting-microservice-50jeh?file=/src/App.tsx

FWIW I just copied/adapted the article and history algorithm from the Redux recipe here, I believe it's the one used in the "redux-undo" library.

https://redux.js.org/recipes/implementing-undo-history

The gist of it is you make this history container type for your store state:

interface Store {
  todos: Todos;
}

interface StateHistory<T> {
  past: Array<T>;
  present: T | null;
  future: Array<T>;
}

let storeStateHistory: StateHistory<Store> = {
  past: [],
  present: null, // probably should't be "null", but whatever
  future: []
};

And then sort of (ab)use a mixture of actionOn listeners and the fact that you still have access to dispatch(), so that you do this:

onChange: actionOn(
      (actions) => [...Object.values(actions), "UNDO", "REDO"], // on every action, plus manual UNDO/REDO dispatch

And in onChange you switch on target.type, and:

  • Record next history if it's not UNDO/REDO
  • Else, mutate state either forwards or backwards instead

easy-peasy-undo-redo

That's a helpful starter, thanks!

  • That's clever, returning ...Object.values(actions) to match every action.
  • Looks like an important observation about reassigning the keys one by one to trigger the proxy..

Some questions:

  • If there are multiple models in the store (e.g. the easypeasy tutorial has two models: Product and Basket), I suppose that every model would need to add it's own actionOn. WDYT?
  • Possibly related, is there any value in trying to attach to easypeasy/redux at a lower level, via e.g. a StoreEnhancer? I don't know how to do that, but it sounds like that's the way redux-devtools works.
  • Is there any easy way to capture the immer patches rather than storing the entire state for each undo? It seems like storing patches rather than full copies of state would be more efficient.
  • I wonder if there are requirements to safely interact with persist()?
  • More generally, I wonder what undo/redo ought do to be a good citizen in the redux world.

@mighdoll It's worth noting that you can attach redux based middleware as part of the store config. You could prob use that in place of actionOn in some form.

@ctrlplusb are the immer patches exposed at all? I didn't see anything in the docs about it but agree with @mighdoll being able to access those would be extremely useful.

OK, I leveled up enough to put together something for undo/redo. It was a bit trickier than expected to navigate to an undo/redo implementation that worked for computed fields, persistence, nested models, and whitelist/blacklisted properties.

To deal with computed fields, I patched easy-peasy to add an additional configuration option in createStore, tentatively named postActionReducer. It allows a user to add a reducer that runs after the normal easy-peasy actions are reduced, but before the computed getters are reattached to the resulting state. Perhaps there are other ways to make undo/redo work, but I thought it would be reasonable for easy-peasy to have an option like this. The key patch is:

  const rootReducer = (state, action) => {
    const stateAfterActions = reducerForActions(state, action);
    const stateAfterCustom =
      _customReducers.length > 0
        ? reducerForCustomReducers(stateAfterActions, action)
        : stateAfterActions;

+    const next = postActionReducer
+      ? postActionReducer(stateAfterCustom, action)
+      : stateAfterCustom;

    if (state !== next) {
      _computedProperties.forEach(({ parentPath, bindComputedProperty }) => {
        const prop = get(parentPath, next);

        // hmm.. this is mutating state during the reduce. 
        if (prop != null) bindComputedProperty(prop);
      });
    }
    return next;
  };

What do you think?

I've published a library containing an undo/redo implementation for easy peasy apps. It's here: patched-undo-peasy.

It uses the state history suggested by @GavinRay97 and redux middleware, as suggested by @jchamb. Thanks for helping out.

The current implementation of undo/redo does not depend on the postActionReducer patch above. However, my app requires a patched version of easy peasy due to #539, so the implementation depends on patched peasy.

That is awesome @mighdoll

More incentive to fix #539 then.

Thanks for merging the #539 patch and publishing 3.4.0-beta.2.

I'll give it a try and republish undo-peasy without the patch.

Oops, my mistake. AFAICT 3.4.0-beta.2 doesn't include the #539 patch. If that's right @ctrlplusb, would you publish a -beta.3?

@mighdoll the v4 release is upcoming and will include this.

Please consider adding your work to the new Community Extensions page contained in the v4 docs.

Should be up by next week. 馃檹

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vincentjames501 picture vincentjames501  路  6Comments

24dev picture 24dev  路  5Comments

positimo picture positimo  路  3Comments

rmariuzzo picture rmariuzzo  路  6Comments

gino8080 picture gino8080  路  5Comments