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 state
const 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;
};