Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Undo / Redo, Howto? #533

Closed
mighdoll opened this issue Aug 9, 2020 · 11 comments
Closed

Undo / Redo, Howto? #533

mighdoll opened this issue Aug 9, 2020 · 11 comments
Labels
docs Stuff that should be added to the docs

Comments

@mighdoll
Copy link
Contributor

mighdoll commented Aug 9, 2020

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

I see some discussion in #124

@GavinRay97
Copy link
Contributor

GavinRay97 commented Aug 9, 2020

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

@mighdoll
Copy link
Contributor Author

mighdoll commented Aug 9, 2020

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.

@jchamb
Copy link
Contributor

jchamb commented Aug 11, 2020

@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.

@mighdoll
Copy link
Contributor Author

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?

@mighdoll
Copy link
Contributor Author

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.

@ctrlplusb
Copy link
Owner

That is awesome @mighdoll

More incentive to fix #539 then.

@ctrlplusb ctrlplusb added the docs Stuff that should be added to the docs label Sep 4, 2020
@mighdoll
Copy link
Contributor Author

mighdoll commented Sep 4, 2020

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.

@mighdoll
Copy link
Contributor Author

mighdoll commented Sep 4, 2020

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?

@ctrlplusb
Copy link
Owner

@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. 🙏

@mighdoll
Copy link
Contributor Author

mighdoll commented Nov 2, 2021 via email

@mighdoll
Copy link
Contributor Author

mighdoll commented Nov 3, 2021

I published a 0.5.0 version of undo-peasy:

  • updates to current easy-peasy
  • initializes first undo state automatically: no need for simple apps to undoSave() or undoReset()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Stuff that should be added to the docs
Projects
None yet
Development

No branches or pull requests

4 participants