Jul 09, 2022
Implement "useUndoRedo" hook
I'm pretty sure you have clicked a lot of Undos and Redos in your life - I know I have. Both of these functions are pretty important in a workflow as they allow us to travel back and forth in time and to fix potential mistakes.
Have you ever wondered what an undo-redo implementation would look like in a computer program? I'm sure there are myriad implementations out there but the fundamentals are pretty much the same:
- A mechanism to store the present value accompanied with the past and future values.
- An algorithm that will appropriately alter the state according to the undo, redo and set action.
Based on the two points above, I believe that a Reducer perfectly fits the use case. It's a function which takes two arguments - the current state and an action - and returns based on both arguments a new state.
Let's try and think how we can utilize a reducer in order to accomplish an undo/redo mechanism.
Point 1: We need to store the past and future values along with the present value.
One way we can accomplish that is by storing the past, present and future values inside an object. Note that past and future values need to be an array in order to travel back and forward anywhere in time.
const state = {
past: [...] // past values
present: ... // present value
future: [...] // future values
}
Point 2: An algorithm for each of the undo, redo and set actions.
For the sake of explaining the algorithm better, let's assume that our counter's state looks like this at some point in the application:
const state = {
past: [0, 1, 2, 3]
present: 4
future: [5, 6]
}
Undo Action
In an undo action, we need to look into the past but we mustn't lose track of the present. So:
- Past: The last value of the past is removed from the array.
- Present: It takes the last value from the past (which is removed as mentioned above).
- Future: We append the present value to the start of the future array.
So, after an undo action our state would become:
const state = {
past: [0, 1, 2]
present: 3
future: [4, 5, 6]
}
Redo Action
In a redo action, we need to look into the future. Hence:
- Past: We push the present value to the end of the past array.
- Present: The first value of the future array now becomes the present value.
- Future: We remove the first element of the future array (which now represents the present value).
const state = {
past: [0, 1, 2, 3]
present: 4
future: [5, 6]
}
Set Action
How would the state need to be adjusted when the set action takes place? E.g when the counter is incremented or decremented?
- Past: The (past) present value now belongs to the past. So we push the value to the past array.
- Present: It's being set with the new value.
- Future: The future is cleared.
const state = {
past: [0, 1, 2, 3, 4]
present: 5
future: []
}
useUndoRedo: React hook implementation
Take a look at this CodeSandbox: https://codesandbox.io/s/react-undo-redo-c3x8pw
By utilizing the useReducer
hook we're able to create a straightforward useUndoRedo
custom hook which looks like setState but provides undo-redo mechanism along with the ability to actually see the past and future values of the state.
The code of the useUndoRedo
hook is shown below.
// use-undo-redo.js
import { useReducer } from "react";
const SET_STATE = "SET_STATE";
const UNDO = "UNDO";
const REDO = "REDO";
const reducerWithUndoRedo = (state, action) => {
const { past, present, future } = state;
switch (action.type) {
case SET_STATE:
return {
past: [...past, present],
present: action.data,
future: []
};
case UNDO:
return {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future]
};
case REDO:
return {
past: [...past, present],
present: future[0],
future: future.slice(1)
};
default:
throw new Error();
}
};
const useUndoRedo = (initialState = {}) => {
const [state, dispatch] = useReducer(reducerWithUndoRedo, {
past: [],
present: initialState,
future: []
});
const { past, present, future } = state;
const setState = (newState) => dispatch({ type: SET_STATE, data: newState });
const undo = () => dispatch({ type: UNDO });
const redo = () => dispatch({ type: REDO });
const isUndoPossible = past && past.length > 0;
const isRedoPossible = future && future.length > 0;
return {
state: present,
setState,
undo,
redo,
pastStates: past,
futureStates: future,
isUndoPossible,
isRedoPossible
};
};
export default useUndoRedo;
The End
I hope you found the article informative.
See you soon! 🙂
Newsletter
Subscribe to my mailing list
Subscribe to get my latest content by email. I won't send you spam, I promise. Unsubscribe at any time.