typescriptintermediate
useUndoRedo State History Hook
Add undo/redo capability to any state with history tracking, max size limits, and keyboard shortcuts.
typescriptPress ⌘/Ctrl + Shift + C to copy
import { useCallback, useRef, useState, useEffect } from 'react';
interface UndoRedoResult<T> {
state: T;
set: (value: T | ((prev: T) => T)) => void;
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
history: T[];
pointer: number;
}
function useUndoRedo<T>(initialState: T, maxHistory = 50): UndoRedoResult<T> {
const [history, setHistory] = useState<T[]>([initialState]);
const [pointer, setPointer] = useState(0);
const set = useCallback((value: T | ((prev: T) => T)) => {
setHistory((prev) => {
const current = prev[pointer];
const newValue = typeof value === 'function'
? (value as (prev: T) => T)(current)
: value;
// Truncate future and append
const newHistory = [...prev.slice(0, pointer + 1), newValue];
// Limit history size
if (newHistory.length > maxHistory) newHistory.shift();
return newHistory;
});
setPointer((p) => Math.min(p + 1, maxHistory - 1));
}, [pointer, maxHistory]);
const undo = useCallback(() => {
setPointer((p) => Math.max(0, p - 1));
}, []);
const redo = useCallback(() => {
setPointer((p) => Math.min(history.length - 1, p + 1));
}, [history.length]);
// Keyboard shortcuts
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
if (e.shiftKey) redo();
else undo();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [undo, redo]);
return {
state: history[pointer],
set,
undo,
redo,
canUndo: pointer > 0,
canRedo: pointer < history.length - 1,
history,
pointer,
};
}
// Usage
function DrawingApp() {
const { state, set, undo, redo, canUndo, canRedo } = useUndoRedo<string[]>([]);
return (
<div className="space-y-3">
<div className="flex gap-2">
<button onClick={undo} disabled={!canUndo} className="px-3 py-1 rounded bg-gray-700 disabled:opacity-30">Undo</button>
<button onClick={redo} disabled={!canRedo} className="px-3 py-1 rounded bg-gray-700 disabled:opacity-30">Redo</button>
<button onClick={() => set((prev) => [...prev, `Item ${prev.length + 1}`])}
className="px-3 py-1 rounded bg-blue-600">Add</button>
</div>
<ul className="text-gray-300">{state.map((item, i) => <li key={i}>{item}</li>)}</ul>
</div>
);
}
export { useUndoRedo };Use Cases
- Text editor undo/redo
- Drawing app history
- Form state recovery
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptbeginner
usePrevious Hook
Track the previous value of any state or prop using a ref-based hook for comparison logic.
Best for: Detecting value changes
#hooks#state
typescriptbeginner
useToggle Hook
Simple boolean toggle hook with set, toggle, on, and off functions.
Best for: Modal open/close state
#hooks#state
typescriptintermediate
useLocalStorage — Persistent State Hook
Sync React state with localStorage including SSR safety, JSON serialization, and cross-tab updates.
Best for: User preferences
#hooks#localstorage
typescriptintermediate
useFormValidation Hook
Lightweight form validation hook with field-level errors, touched tracking, and submit handling.
Best for: Contact forms
#hooks#forms