-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathuseUndoable.ts
89 lines (84 loc) · 2.44 KB
/
useUndoable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { useCallback, useRef } from 'react';
/**
* Wraps a state hook to add undo/redo functionality.
*
* @param useStateResult Return value of a state hook.
* @param useStateResult.0 Current state.
* @param useStateResult.1 State updater function.
* @returns State hook result extended with `undo`, `redo`, `pastValues` and `futureValues`.
*
* @example
* function Example() {
* const [value, setValue, undo, redo, pastValues, futureValues] = useUndoable(
* useState(''),
* );
* // ...
* return (
* <>
* <button type="button" onClick={undo} disabled={pastValues.length === 0}>
* Undo
* </button>
* <input value={value} onChange={e => setValue(e.target.value)} />
* <button type="button" onClick={redo} disabled={futureValues.length === 0}>
* Redo
* </button>
* </>
* );
* }
*/
export default function useUndoable<T>([value, setValue]: [
T,
React.Dispatch<React.SetStateAction<T>>,
]): [
T,
React.Dispatch<React.SetStateAction<T>>,
() => void,
() => void,
T[],
T[],
] {
// Source: https://redux.js.org/recipes/implementing-undo-history
const pastValuesRef = useRef<T[]>([]);
const futureValuesRef = useRef<T[]>([]);
const newSetValue = useCallback(
(update: React.SetStateAction<T>) => {
setValue(prevValue => {
futureValuesRef.current = [];
pastValuesRef.current = [...pastValuesRef.current, prevValue];
return typeof update === 'function'
? (update as (prevValue: T) => T)(prevValue)
: update;
});
},
[setValue],
);
const undo = useCallback(() => {
if (pastValuesRef.current.length > 0) {
setValue(prevValue => {
const nextValue =
pastValuesRef.current[pastValuesRef.current.length - 1];
pastValuesRef.current = pastValuesRef.current.slice(0, -1);
futureValuesRef.current = [prevValue, ...futureValuesRef.current];
return nextValue;
});
}
}, [setValue]);
const redo = useCallback(() => {
if (futureValuesRef.current.length > 0) {
setValue(prevValue => {
const nextValue = futureValuesRef.current[0];
futureValuesRef.current = futureValuesRef.current.slice(1);
pastValuesRef.current = [...pastValuesRef.current, prevValue];
return nextValue;
});
}
}, [setValue]);
return [
value,
newSetValue,
undo,
redo,
pastValuesRef.current,
futureValuesRef.current,
];
}