import {ReducerAction, ReducerState, useReducer} from 'react'; export type UndoableNode = { current: S; next: UndoableNode | undefined; previous: UndoableNode | undefined; }; type UndoAction = { type: 'undo'; }; type RedoAction = { type: 'redo'; }; export type UndoableReducerAction = UndoAction | RedoAction | A; export type UndoableReducer> = React.Reducer< UndoableNode>, UndoableReducerAction> >; function isUndoOrRedoAction( action: UndoableReducerAction ): action is UndoAction | RedoAction { if (action.type) { return action.type === 'undo' || action.type === 'redo'; } return false; } function undoableReducer( state: UndoableNode, action: UndoAction | RedoAction ): UndoableNode { if (action.type === 'undo') { return state.previous === undefined ? state : state.previous; } if (action.type === 'redo') { return state.next === undefined ? state : state.next; } throw new Error('Unreachable case'); } export function makeUndoableReducer>( reducer: R ): UndoableReducer { return ( state: UndoableNode>, action: UndoableReducerAction> ) => { if (isUndoOrRedoAction(action)) { return undoableReducer(state, action); } const newState: UndoableNode> = { next: undefined, previous: state, current: reducer(state.current, action), }; state.next = newState; return newState; }; } export function useUndoableReducer< R extends React.Reducer, ReducerAction> >( reducer: R, initialState: ReducerState ): [ ReducerState, React.Dispatch>>, { nextState: ReducerState | undefined; previousState: ReducerState | undefined; } ] { const [state, dispatch] = useReducer(makeUndoableReducer(reducer), { current: initialState, previous: undefined, next: undefined, }); return [ state.current, dispatch, {previousState: state.previous?.current, nextState: state.next?.current}, ]; }