Skip to content

Instantly share code, notes, and snippets.

@X-Y
Forked from johanquiroga/useUndoReducer.js
Last active November 3, 2022 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save X-Y/028284a2fd7091d2d3fa3034e829f3d7 to your computer and use it in GitHub Desktop.
Save X-Y/028284a2fd7091d2d3fa3034e829f3d7 to your computer and use it in GitHub Desktop.
Undo/Redo capability for any reducer using react hook `useReducer`
import { useReducer, Dispatch } from "react";
export enum USE_UNDO_REDUCER_TYPES {
undo = "UNDO",
redo = "REDO",
}
export type UndoRedoAction = {
type: USE_UNDO_REDUCER_TYPES;
};
export type HistoryType<T> = {
past: T[];
present: T;
future: T[];
};
const isTypedAction = (action: any): action is UndoRedoAction => {
return (
"type" in action &&
(action.type === USE_UNDO_REDUCER_TYPES.redo ||
action.type === USE_UNDO_REDUCER_TYPES.undo)
);
};
const useUndoReducer = <A, T>(
reducer: (prevState: T, action: A) => T,
initialState: T
) => {
const undoState: HistoryType<T> = {
past: [],
present: initialState,
future: [],
};
const undoReducer = (state: typeof undoState, action: A | UndoRedoAction) => {
if (isTypedAction(action)) {
if (action.type === USE_UNDO_REDUCER_TYPES.undo) {
const [newPresent, ...past] = state.past;
return {
past,
present: newPresent,
future: [state.present, ...state.future],
};
}
if (action.type === USE_UNDO_REDUCER_TYPES.redo) {
const [newPresent, ...future] = state.future;
return {
past: [state.present, ...state.past],
present: newPresent,
future,
};
}
throw "shouldn't be here";
} else {
const newPresent = reducer(state.present, action);
return {
past: [state.present, ...state.past],
present: newPresent,
future: [],
};
}
};
const [state, dispatch] = useReducer(undoReducer, undoState);
return [state.present, dispatch, state] as [
T,
Dispatch<A | UndoRedoAction>,
HistoryType<T>
];
};
export default useUndoReducer;
// Example
/* import React, { useReducer, Dispatch } from "react";
import useUndoReducer from "./useUndoReducer";
const initialState = {
foo: '123'
}
type ActionType = {
type: 'action1',
} | {
type: 'action2',
payload: ...
} | ...
const reducer = (state = initialState, action: ActionType) => {
// Your reducer
return state;
};
type MyDispatch = Dispatch<ActionType>
type MyHistory = HistoryType<typeof initialState>
const YourComponent = (props) => {
const [state, dispatch, history] = useUndoReducer(reducer, initialState);
// Some actions
const someAction = useCallback(
(payload) => {
dispatch({
type: 'SOME_ACTION',
payload,
});
},
[dispatch]
);
// Undo/Redo actions
const undo = useCallback(() => {
dispatch({ type: USE_UNDO_REDUCER_TYPES.undo });
}, [dispatch]);
const redo = useCallback(() => {
dispatch({ type: USE_UNDO_REDUCER_TYPES.redo });
}, [dispatch]);
// Check if on top of the history stack
const isMostRecent = !history.future.length
// render or do something something
<div>
{state.foo}
</div>
// ...
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment