-
-
Save X-Y/028284a2fd7091d2d3fa3034e829f3d7 to your computer and use it in GitHub Desktop.
Undo/Redo capability for any reducer using react hook `useReducer`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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