history
Add undo and redo to any chunk with built-in history tracking.
history() 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 { history } from "stunk/middleware";
const count = chunk(0);
const tracked = history(count);
tracked.set(1);
tracked.set(2);
tracked.set(3);
tracked.undo(); // 2
tracked.undo(); // 1
tracked.redo(); // 2API reference
undo()
Reverts to the previous value. Does nothing if at the beginning of history.
tracked.undo();redo()
Moves forward to the next value. Does nothing if at the end of history.
tracked.redo();canUndo()
Returns true if there is a previous state to revert to.
if (tracked.canUndo()) tracked.undo();canRedo()
Returns true if there is a next state to move forward to.
if (tracked.canRedo()) tracked.redo();getHistory()
Returns a copy of all recorded values, oldest first.
tracked.set(10);
tracked.set(20);
tracked.getHistory(); // [0, 10, 20]clearHistory()
Wipes the history stack, keeping only the current value as the new baseline.
tracked.clearHistory();
tracked.getHistory(); // [currentValue]
tracked.canUndo(); // falsereset()
Resets the chunk to its initial value and clears the entire history stack. After reset, canUndo() and canRedo() both return false.
tracked.set(1);
tracked.set(2);
tracked.reset();
tracked.get(); // 0 (initial value)
tracked.canUndo(); // false — history cleared
tracked.canRedo(); // falseIn v3, reset() is overridden to also clear history. In v2, calling reset()
only reset the value but left history intact — which meant canUndo() could
return true even after a reset.
Options
| Option | Type | Default | Description |
|---|---|---|---|
maxHistory | number | 100 | Maximum history entries. Oldest entries removed when limit is reached |
skipDuplicates | boolean | "shallow" | false | Skip recording identical values. See below. |
const tracked = history(count, { maxHistory: 50 });skipDuplicates
Control whether consecutive identical values are recorded:
// true — skips strictly equal values (===)
const tracked = history(count, { skipDuplicates: true });
tracked.set(5);
tracked.set(5); // skipped — same primitive value
tracked.getHistory(); // [0, 5]
// 'shallow' — also skips shallowly equal objects
const form = history(formChunk, { skipDuplicates: "shallow" });
form.set({ name: "Fola", age: 25 });
form.set({ name: "Fola", age: 25 }); // skipped — same flat valuesskipDuplicates: true uses strict equality (===) — new objects with the
same values are not skipped. Use 'shallow' for flat object comparison.
Branching history
set() after an undo() discards all forward history — just like most editors:
tracked.set(1);
tracked.set(2);
tracked.set(3);
tracked.undo(); // back to 2
tracked.set(99); // history is now [0, 1, 2, 99] — "3" is gone
tracked.canRedo(); // falseIn React
import { chunk } from "stunk";
import { history } from "stunk/middleware";
import { useChunk } from "stunk/react";
const counter = chunk(0);
const tracked = history(counter);
function Counter() {
const [count, setCount] = useChunk(tracked);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<button onClick={() => tracked.undo()} disabled={!tracked.canUndo()}>
Undo
</button>
<button onClick={() => tracked.redo()} disabled={!tracked.canRedo()}>
Redo
</button>
</div>
);
}