useUndoableReducer.tsx 2.1 KB

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