21
loading...
This website collects cookies to deliver better user experience
import { useMemo, useState } from "react";
// If you're only working with primitives, this is not required
import isEqual from "lodash/isEqual";
export default function useUndoableState(init) {
const [states, setStates] = useState([init]); // Used to store history of all states
const [index, setIndex] = useState(0); // Index of current state within `states`
const state = useMemo(() => states[index], [states, index]); // Current state
const setState = (value) => {
// Use lodash isEqual to check for deep equality
// If state has not changed, return to avoid triggering a re-render
if (isEqual(state, value)) {
return;
}
const copy = states.slice(0, index + 1); // This removes all future (redo) states after current index
copy.push(value);
setStates(copy);
setIndex(copy.length - 1);
};
// Clear all state history
const resetState = (init) => {
setIndex(0);
setStates([init]);
};
// Allows you to go back (undo) N steps
const goBack = (steps = 1) => {
setIndex(Math.max(0, Number(index) - (Number(steps) || 1)));
};
// Allows you to go forward (redo) N steps
const goForward = (steps = 1) => {
setIndex(Math.min(states.length - 1, Number(index) + (Number(steps) || 1)));
};
return {
state,
setState,
resetState,
index,
lastIndex: states.length - 1,
goBack,
goForward,
};
}
index
(number) and states
(array). states
stores the historical values of the state while index
determines current state by indicating the current position in the array.goBack
and goForward
functions emitted by the hook. However, if you make a call to setState
and index
is not at the end of the states
array, all states after index
is erased and index
will go back to the end of the states
array. In other words, once you call setState
, you can no longer redo.Prop | Type | Usage | Description |
---|---|---|---|
state | any |
Current state, initialised with argument passed | |
setState | func |
setState(value) |
Sets state to value . All values after current index is erased |
resetState | func |
resetState(value) |
Deletes historical states and resets to value |
index | number |
The current index in the states array |
|
lastIndex | number |
The last index in the states array. Can be used to determine if can goForward . canGoForward = index < lastIndex
|
|
goBack | func |
goBack(2) |
Goes back the number of steps passed |
goForward | func |
goForward(3) |
Goes forward the number of steps passed |
import React from "react";
import useUndoableState from "path/to/hook";
const init = { text: "The quick brown fox jumps over the lazy dog" };
export default function Document() {
const {
state: doc,
setState: setDoc,
resetState: resetDoc,
index: docStateIndex,
lastIndex: docStateLastIndex,
goBack: undoDoc,
goForward: redoDoc
} = useUndoableState(init);
const canUndo = docStateIndex > 0;
const canRedo = docStateIndex < docStateLastIndex;
return (
<div style={{ display: "block" }}>
<textarea
style={{ margin: "16px" }}
onChange={(event) => setDoc({ text: event.target.value })}
rows="5"
value={doc.text}
/>
<div>
<button
onClick={() => undoDoc()}
disabled={!canUndo}
style={{ marginRight: "8px" }}
>
Undo
</button>
<button
onClick={() => redoDoc()}
disabled={!canRedo}
style={{ marginRight: "8px" }}
>
Redo
</button>
<button onClick={() => resetDoc(init)}>Reset</button>
</div>
</div>
);
}