Undo/redo is a beloved feature and a fiddly one. The core is a useState that doesn't forget: every value it's held is recorded, and you can walk back and forward through that timeline. useStateWithHistory is that primitive — it behaves like useState, but also tracks a history array and a pointer into it, with back, forward, and go to move around.
Implement useStateWithHistory(initialValue, { capacity = 10 }). Return [value, set, controls] where controls has history, pointer, back(steps), forward(steps), and go(index). Setting a new value after going back truncates the forward history (a new branch, like a browser's back-then-navigate), and history never grows past capacity.
function useStateWithHistory(initialValue, { capacity = 10 }) {
// returns [value, set, { history, pointer, back, forward, go }]
}
const [text, setText, { back, forward }] = useStateWithHistory('');
setText('h'); setText('hi'); // history: ['', 'h', 'hi']
back(); // text === 'h' (undo)
forward(); // text === 'hi' (redo)
const [v, set, { go, history }] = useStateWithHistory(0);
set(1); set(2);
go(0); // back to 0
set(9); // branch: history is now [0, 9], the 1/2 future is discarded
value is history[pointer]; back/forward move the pointer (clamped), go(i) jumps to it.set while the pointer is behind the end drops everything after it, then appends — you can't redo into the old future.set to the same value shouldn't add a duplicate history entry.