useUndoableReducer.tsx 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import type {ReducerAction, ReducerState} from 'react';
  2. import {useMemo, useReducer} from 'react';
  3. export type UndoableNode<S> = {
  4. current: S;
  5. next: UndoableNode<S> | undefined;
  6. previous: UndoableNode<S> | undefined;
  7. };
  8. type UndoAction = {
  9. type: 'undo';
  10. };
  11. type RedoAction = {
  12. type: 'redo';
  13. };
  14. export type UndoableReducerAction<A> = UndoAction | RedoAction | A;
  15. export type UndoableReducer<R extends React.Reducer<any, any>> = React.Reducer<
  16. UndoableNode<ReducerState<R>>,
  17. UndoableReducerAction<ReducerAction<R>>
  18. >;
  19. function isUndoOrRedoAction(
  20. action: UndoableReducerAction<any>
  21. ): action is UndoAction | RedoAction {
  22. if (action.type) {
  23. return action.type === 'undo' || action.type === 'redo';
  24. }
  25. return false;
  26. }
  27. function undoableReducer<S>(
  28. state: UndoableNode<S>,
  29. action: UndoAction | RedoAction
  30. ): UndoableNode<S> {
  31. if (action.type === 'undo') {
  32. return state.previous === undefined ? state : state.previous;
  33. }
  34. if (action.type === 'redo') {
  35. return state.next === undefined ? state : state.next;
  36. }
  37. throw new Error('Unreachable case');
  38. }
  39. export function makeUndoableReducer<R extends React.Reducer<any, any>>(
  40. reducer: R
  41. ): UndoableReducer<R> {
  42. return (
  43. state: UndoableNode<ReducerState<R>>,
  44. action: UndoableReducerAction<ReducerAction<R>>
  45. ) => {
  46. if (isUndoOrRedoAction(action)) {
  47. return undoableReducer(state, action);
  48. }
  49. const newState: UndoableNode<ReducerState<R>> = {
  50. next: undefined,
  51. previous: state,
  52. current: reducer(state.current, action),
  53. };
  54. state.next = newState;
  55. return newState;
  56. };
  57. }
  58. type UndoableReducerState<R extends React.Reducer<ReducerState<R>, ReducerAction<R>>> = [
  59. ReducerState<R>,
  60. React.Dispatch<UndoableReducerAction<ReducerAction<R>>>,
  61. {
  62. nextState: ReducerState<R> | undefined;
  63. previousState: ReducerState<R> | undefined;
  64. },
  65. ];
  66. export function useUndoableReducer<
  67. R extends React.Reducer<ReducerState<R>, ReducerAction<R>>,
  68. >(reducer: R, initialState: ReducerState<R>): UndoableReducerState<R> {
  69. const [state, dispatch] = useReducer(makeUndoableReducer(reducer), {
  70. current: initialState,
  71. previous: undefined,
  72. next: undefined,
  73. });
  74. const value: UndoableReducerState<R> = useMemo(() => {
  75. return [
  76. state.current,
  77. dispatch,
  78. {previousState: state.previous?.current, nextState: state.next?.current},
  79. ];
  80. }, [state, dispatch]);
  81. return value;
  82. }