Implementing Undo and Redo in React
{past: [allPastStates],present: currentState,future: [anyAndAllFutureStates],}
Code before:
const reducer = (state = [], action) => {if (action.type === GRUDGE_ADD) {return [{id: id(),...action.payload},...state];}if (action.type === GRUDGE_FORGIVE) {return state.map(grudge => {if (grudge.id === action.payload.id) {return { ...grudge, forgiven: !grudge.forgiven };}return grudge;});}return state;};export const GrudgeProvider = ({ children }) => {const [grudges, dispatch] = useReducer(reducer, initialState);const addGrudge = useCallback(({ person, reason }) => {dispatch({type: GRUDGE_ADD,payload: {person,reason}});},[dispatch]);const toggleForgiveness = useCallback(id => {dispatch({type: GRUDGE_FORGIVE,payload: {id}});},[dispatch]);return (<GrudgeContext.Provider value={{ grudges, addGrudge, toggleForgiveness }}>{children}</GrudgeContext.Provider>);};
Code after:
const reducer = (state = defaultState, action) => {if (action.type === GRUDGE_ADD) {const newPresent = [{id: id(),...action.payload},...state.present,];return {past: [state.present, ...state.past],present: newPresent,future: [],};}if (action.type === GRUDGE_FORGIVE) {const newPresent = state.present.map(grudge => {if ((grudge.id === action.payload.id)) {return { ...grudge, forgiven: !grudge.forgiven}}return grudge;});return {past: [state.present, ...state.past],present: newPresent,future: [],};}if (action.type === 'UNDO') {const [newPresent, ...newPast] = state.past;return {past: newPast,present: newPresent,future: [state.present, ...state.future],}}if (action.type === 'REDO') {const [newPresent, ...newFuture] = state.future;return {past: [state.present, ...state.past],present: newPresent,future: newFuture,}}return state;};const defaultState = {past: [],present: initialState,future: [],};export const GrudgeProvider = ({ children }) => {const [state, dispatch] = useReducer(reducer, initialState);const grudges = state.present;const isPast = !!state.past.length;const isFuture = !!state.future.length;/**Everything else is the same */const undo = useCallback(()=> {dispatch({type: 'UNDO'});}, [dispatch]);const redo = useCallback(() => {dispatch({type: 'REDO'});}, [dispatch]);return (<GrudgeContext.Provider value={{ grudges, addGrudge, toggleForgiveness, undo, isPast, redo, isFuture }}>{children}</GrudgeContext.Provider>);};
How to wrap the timeframe logic: create a specific reducer
const useUndoReducer = (reducer, intiialState) => {const undoState = {past: [],present: initialstate,future: [],}const undoReducer = (state, action) => {// The reducer only get the present stateconst newPresent = reducer(state.present, action);if (action.type === 'UNDO') {const [newPresent, ...newPast] = state.past;return {past: newPast,present: newPresent,future: [state.present, ...state.future],}}if (action.type === 'REDO') {const [newPresent, ...newFuture] = state.future;return {past: [state.present, ...state.past],present: newPresent,future: newFuture,};}return {past: [state.present, ...state.past],present: state.newPresent,future: [],}}return useReducer(undoReducer, undoState);};const reducer = (state = initialState, action) => {if (action.type === GRUDGE_ADD) {return [{id: id(),...action.payload},...state,];}if (action.type === GRUDGE_FORGIVE) {return state.map(grudge => {if ((grudge.id === action.payload.id)) {return { ...grudge, forgiven: !grudge.forgiven}}return grudge;});}if (action.type === 'UNDO') {const [newPresent, ...newPast] = state.past;return {past: newPast,present: newPresent,future: [state.present, ...state.future],}}if (action.type === 'REDO') {const [newPresent, ...newFuture] = state.future;return {past: [state.present, ...state.past],present: newPresent,future: newFuture,}}return state;};