xplato / useUndoable

↪ React hook for undo/redo functionality (with batteries included)
MIT License
168 stars 7 forks source link

Helper for live interaction #32

Closed vallsv closed 5 months ago

vallsv commented 6 months ago

Hi,

I am working on complex interaction with multiple components supporting drag and drop.

I found out that it was much easier to handle that by wrapping your hook inside and own code, allowing me to handle a "live" state which is not yet part of the history.

For example during a mouse drag, all my components are aware of the new state, but this state is not yet part of the history. At the end of the mouse interaction it be came part of the history.

I call it "intermediate" state.

Would you be interested in such behavior, or by chance is there a way to setup your hook to behave this way?

Here is my actual code:

import { useMemo, useState } from 'react';
import useUndoable from 'use-undoable';

/**
 * A useUndoable hook featuring an intermediate present state
 * which is not yet archived in the history.
 *
 * This simplifies a lot interaction between multiple components.
 */
export default function useUndoableWithIntermediate<T>(props: T): [
  T,
  (state: Partial<T>, intermediate?: boolean) => void,
  {
    past: T[];
    future: T[];
    undo: () => void;
    canUndo: boolean;
    redo: () => void;
    canRedo: boolean;
    reset: (initialState?: T) => void;
    resetInitialState: (newInitialState: T) => void;
  }
] {
  const [present, setPresent, actions] = useUndoable<T>(props);
  const [intermediate, setIntermediate] = useState<Partial<T> | undefined>(
    undefined
  );

  const state = useMemo(() => {
    return { ...present, ...intermediate };
  }, [present, intermediate]);

  function setState(value: Partial<T>, intermediate?: boolean) {
    if (intermediate) {
      setIntermediate((state) => {
        return { ...state, ...value };
      });
    } else {
      setPresent((state: T) => {
        return { ...state, ...value };
      });
      setIntermediate(() => undefined);
    }
  }

  return [state, setState, actions];
}

And I use it this way:

# During the drag interaction
setState(newState, true);
# At the end of the drag
setState(newState, false);

It works pretty well.

xplato commented 6 months ago

Hey @vallsv, I like this solution! I'm wondering if this achieves the same effect as the ignoreAction param on setState, however. Take a look at this crude example I whipped up in CodeSandbox.

The whole state is tracked via useUndoable here. Every time onMouseMove is called, it'll call setPosition with the final param, ignoreAction, set to true—this way, it'll only update the present state. However, 300 milliseconds after you stop moving the mouse, setPosition is called normally, and the state is updated as you'd expect.

It's a crude and not-very-performant example, but it demonstrates how events with a huge quantity of calls can be handled without polluting the state.

Does this generally fit your requirement or is there something else I'm not considering that your useUndoableWithIntermediate hook covers?

Thanks for opening an issue, by the way! 😄

vallsv commented 6 months ago

Thanks for your feedback. Sounds like the same use case.

I unfortunately don't have much time to play with it now , But ill go back to this task in couple of months.

Feel free to close this issue if you prefer, i could reopen it in case.

xplato commented 5 months ago

No worries! Feel free to reopen it when you get back to it :)