useUndoRedo
Time-travel state management with undo/redo functionality. Perfect for editors, drawing apps, and form builders.
Live Demo
Draw shapes and use undo/redo to travel through history. Click any history state to jump to it.
šØ Drawing Canvas
ā®ļø Time Travel Controls
Shapes: 0
History: 1 / 1
Can Undo: ā | Can Redo: ā
š History Timeline
š” How It Works:
- Every state change is saved to history
- Navigate backward and forward through states
- Jump to any point in history
- New changes clear future history
- Configurable max history size
Code Examples
Basic Usage
import { useUndoRedo } from "@uiblock/hooks";
function TextEditor() {
const {
state,
setState,
undo,
redo,
canUndo,
canRedo
} = useUndoRedo({
initialState: { text: '' },
maxHistory: 50
});
return (
<div>
<textarea
value={state.text}
onChange={(e) => setState({ text: e.target.value })}
/>
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
</div>
);
}Form Builder
import { useUndoRedo } from "@uiblock/hooks";
interface FormField {
id: string;
type: 'text' | 'email' | 'number';
label: string;
}
function FormBuilder() {
const { state, setState, undo, redo, canUndo, canRedo } = useUndoRedo<{
fields: FormField[]
}>({
initialState: { fields: [] }
});
const addField = (type: FormField['type']) => {
setState(prev => ({
fields: [...prev.fields, {
id: Date.now().toString(),
type,
label: `New ${type} field`
}]
}));
};
const removeField = (id: string) => {
setState(prev => ({
fields: prev.fields.filter(f => f.id !== id)
}));
};
return (
<div>
<button onClick={() => addField('text')}>Add Text Field</button>
<button onClick={undo} disabled={!canUndo}>ā¶ Undo</button>
<button onClick={redo} disabled={!canRedo}>ā· Redo</button>
{state.fields.map(field => (
<div key={field.id}>
<input type={field.type} placeholder={field.label} />
<button onClick={() => removeField(field.id)}>Remove</button>
</div>
))}
</div>
);
}History Timeline
import { useUndoRedo } from "@uiblock/hooks";
function HistoryViewer() {
const {
state,
history,
currentIndex,
goToState
} = useUndoRedo({
initialState: { count: 0 }
});
return (
<div>
<p>Current: {state.count}</p>
<div>
{history.map((historyState, index) => (
<button
key={index}
onClick={() => goToState(index)}
style={{
fontWeight: index === currentIndex ? 'bold' : 'normal'
}}
>
State {index}: {historyState.count}
</button>
))}
</div>
</div>
);
}Keyboard Shortcuts
import { useUndoRedo } from "@uiblock/hooks";
import { useEffect } from "react";
function EditorWithShortcuts() {
const { state, setState, undo, redo, canUndo, canRedo } = useUndoRedo({
initialState: { content: '' }
});
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
e.preventDefault();
if (e.shiftKey) {
redo();
} else {
undo();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [undo, redo]);
return (
<textarea
value={state.content}
onChange={(e) => setState({ content: e.target.value })}
placeholder="Try Cmd/Ctrl+Z to undo, Cmd/Ctrl+Shift+Z to redo"
/>
);
}How It Works
Here's the implementation:
import { useState, useCallback, useRef } from 'react'
export function useUndoRedo<T>(options: {
maxHistory?: number
initialState: T
}) {
const { maxHistory = 50, initialState } = options
const [history, setHistory] = useState<T[]>([initialState])
const [currentIndex, setCurrentIndex] = useState(0)
const isUndoRedoRef = useRef(false)
const state = history[currentIndex]
const setState = useCallback((newState: T | ((prev: T) => T)) => {
if (isUndoRedoRef.current) {
isUndoRedoRef.current = false
return
}
setHistory(currentHistory => {
const resolvedState = typeof newState === 'function'
? (newState as (prev: T) => T)(currentHistory[currentIndex])
: newState
// Remove any future history when making a new change
const newHistory = currentHistory.slice(0, currentIndex + 1)
newHistory.push(resolvedState)
// Limit history size
if (newHistory.length > maxHistory) {
newHistory.shift()
setCurrentIndex(currentIndex)
return newHistory
}
setCurrentIndex(newHistory.length - 1)
return newHistory
})
}, [currentIndex, maxHistory])
const undo = useCallback(() => {
if (currentIndex > 0) {
isUndoRedoRef.current = true
setCurrentIndex(currentIndex - 1)
}
}, [currentIndex])
const redo = useCallback(() => {
if (currentIndex < history.length - 1) {
isUndoRedoRef.current = true
setCurrentIndex(currentIndex + 1)
}
}, [currentIndex, history.length])
const reset = useCallback(() => {
setHistory([initialState])
setCurrentIndex(0)
}, [initialState])
const goToState = useCallback((index: number) => {
if (index >= 0 && index < history.length) {
isUndoRedoRef.current = true
setCurrentIndex(index)
}
}, [history.length])
return {
state,
setState,
undo,
redo,
canUndo: currentIndex > 0,
canRedo: currentIndex < history.length - 1,
reset,
history,
currentIndex,
goToState
}
}API Reference
useUndoRedo(options)
initialState: TInitial state value
maxHistory?: numberMaximum history size (default: 50)
Returns:
- state: Current state
- setState: Update state function
- undo: Go back one step
- redo: Go forward one step
- canUndo: Boolean if undo is available
- canRedo: Boolean if redo is available
- reset: Reset to initial state
- history: Array of all states
- currentIndex: Current position in history
- goToState: Jump to specific history index
š” Use Cases
- Text editors and rich text editors
- Drawing and design tools
- Form builders and visual editors
- Game state management
- Configuration editors
- Any app needing undo/redo functionality