withHistory
Add undo and redo to any chunk with built-in history tracking.
withHistory() wraps an existing chunk and adds a full undo/redo history stack to it. Every set() call is recorded — you can travel backwards and forwards through the state timeline.
import { chunk } from "stunk";
import { withHistory } from "stunk/middleware";
const count = chunk(0);
const countWithHistory = withHistory(count);
countWithHistory.set(1);
countWithHistory.set(2);
countWithHistory.set(3);
countWithHistory.undo(); // 2
countWithHistory.undo(); // 1
countWithHistory.redo(); // 2API reference
undo()
Reverts to the previous value in history. Does nothing if there's no previous state.
countWithHistory.undo();redo()
Moves forward to the next value in history. Does nothing if there's no next state.
countWithHistory.redo();canUndo()
Returns true if there is a previous state to revert to.
if (countWithHistory.canUndo()) {
countWithHistory.undo();
}canRedo()
Returns true if there is a next state to move forward to.
if (countWithHistory.canRedo()) {
countWithHistory.redo();
}getHistory()
Returns a copy of all recorded values in order, oldest first.
countWithHistory.set(10);
countWithHistory.set(20);
countWithHistory.getHistory(); // [0, 10, 20]clearHistory()
Wipes the history stack, keeping only the current value as the new baseline.
countWithHistory.clearHistory();
countWithHistory.getHistory(); // [currentValue]
countWithHistory.canUndo(); // falseOptions
| Option | Type | Default | Description |
|---|---|---|---|
maxHistory | number | 100 | Maximum number of history entries to keep. Oldest entries are removed when the limit is reached. |
const countWithHistory = withHistory(count, { maxHistory: 50 });When the history limit is reached, the oldest entries are removed automatically and a warning is logged to the console.
Branching history
When you set() a new value after undoing, all future history is discarded — just like most editors:
countWithHistory.set(1);
countWithHistory.set(2);
countWithHistory.set(3);
countWithHistory.undo(); // back to 2
countWithHistory.set(99); // history is now [0, 1, 2, 99] — "3" is gone
countWithHistory.canRedo(); // falseIn React
import { chunk } from "stunk";
import { withHistory } from "stunk/middleware";
import { useChunk } from "stunk/react";
const counter = chunk(0);
const counterWithHistory = withHistory(counter);
function Counter() {
const [count, setCount] = useChunk(counterWithHistory);
const canUndo = counterWithHistory.canUndo();
const canRedo = counterWithHistory.canRedo();
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<button onClick={() => counterWithHistory.undo()} disabled={!canUndo}>
Undo
</button>
<button onClick={() => counterWithHistory.redo()} disabled={!canRedo}>
Redo
</button>
</div>
);
}Since useChunk subscribes to the chunk and calls setState on every change, the component re-renders whenever the value updates — and canUndo() / canRedo() are re-evaluated as part of that render, keeping the buttons in sync automatically.