Skip to content

Instantly share code, notes, and snippets.

@johanquiroga
Last active April 14, 2025 15:00
Show Gist options
  • Save johanquiroga/cbbfc0da2e9f11d2dbfd15acc4db6fc0 to your computer and use it in GitHub Desktop.
Save johanquiroga/cbbfc0da2e9f11d2dbfd15acc4db6fc0 to your computer and use it in GitHub Desktop.
Undo/Redo capability for any reducer using react hook `useReducer`
import { useReducer } from 'react';
const useUndoReducer = (reducer, initialState) => {
const undoState = {
past: [],
present: initialState,
future: []
};
const undoReducer = (state, action) => {
const newPresent = reducer(state.present, action);
if (action.type === useUndoReducer.types.undo) {
const [newPresent, ...past] = state.past;
return {
past,
present: newPresent,
future: [state.present, ...state.future]
};
}
if (action.type === useUndoReducer.types.redo) {
const [newPresent, ...future] = state.future;
return {
past: [state.present, ...state.past],
present: newPresent,
future
};
}
return {
past: [state.present, ...state.past],
present: newPresent,
future: []
};
};
return useReducer(undoReducer, undoState);
};
useUndoReducer.types = {
undo: 'UNDO',
redo: 'REDO'
};
export default useUndoReducer;
// Example
/* import React, { useReducer } from "react";
import useUndoReducer from "./useUndoReducer";
const initialState = {}
const reducer = (state = initialState, action) => {
// Your reducer
return state;
};
const YourComponent = (props) => {
const [state, dispatch] = useUndoReducer(reducer, initialState);
// Some actions
const someAction = useCallback(
(payload) => {
dispatch({
type: 'SOME_ACTION',
payload,
});
},
[dispatch]
);
// Undo/Redo actions
const undo = useCallback(() => {
dispatch({ type: useUndoReducer.types.undo });
}, [dispatch]);
const redo = useCallback(() => {
dispatch({ type: useUndoReducer.types.redo });
}, [dispatch]);
// render or do something something
// ...
}
*/
@wladpaiva
Copy link

For anyone looking for a typescript version of this

import { useReducer } from "react";

interface UndoState<S> {
  past: S[];
  present: S;
  future: S[];
}

type UndoAction = { type: "UNDO" } | { type: "REDO" };

export const useUndoReducer = <S, A extends { type: string }>(
  reducer: (prevState: S, ...args: [A]) => S,
  initialState: S
) => {
  const undoState: UndoState<S> = {
    past: [],
    present: initialState,
    future: [],
  };

  const undoReducer = (
    state: UndoState<S>,
    action: A | UndoAction
  ): UndoState<S> => {
    const newPresent = reducer(state.present, action as A);

    if (action.type === useUndoReducer.types.undo) {
      const [newPresent, ...past] = state.past;
      return {
        past,
        present: newPresent,
        future: [state.present, ...state.future],
      };
    }

    if (action.type === useUndoReducer.types.redo) {
      const [newPresent, ...future] = state.future;
      return {
        past: [state.present, ...state.past],
        present: newPresent,
        future,
      };
    }

    return {
      past: [state.present, ...state.past],
      present: newPresent,
      future: [],
    };
  };

  return useReducer(undoReducer, undoState);
};

useUndoReducer.types = {
  undo: "UNDO",
  redo: "REDO",
} as const;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment