The Brand New 20-Step Undo/Redo System is a high-performance circular buffer implementation that replaces the previous 50-step system. It features automatic state saving, thumbnail generation, and an integrated visual history stack in the Layer drawer.
Primary hook implementing the undo/redo system with circular buffer
Location: Line 57
Parameters:
maxHistory(number, default: 20) - Maximum history stepsthrottleMs(number, default: 300) - Throttle delay for save operationsincludeThumbnail(boolean, default: true) - Whether to generate thumbnails
Returns: UseUndoRedoReturn object with state and methods
history: HistoryState[]- Array of history statescurrentIndex: number- Current position in history (-1 = empty)canUndo: boolean- Whether undo is possiblecanRedo: boolean- Whether redo is possibletotalStates: number- Total number of states saved
Location: Line 102
- Saves current canvas state to history
- Throttled to prevent excessive saves
- Creates JSON snapshot and optional thumbnail
- Manages circular buffer (removes oldest when max exceeded)
- Sets
skipSaveRefandisRestoringRefto prevent recursive saves
Location: Line 162
- Restores previous state
- Sets
isRestoringRef.current = trueto prevent auto-save during restoration - Uses
canvas.loadFromJSON()to restore state - Updates
currentIndexRefand state - Calls optional callback after completion
Location: Line 202
- Restores next state (if available)
- Same mechanism as
undo()but moves forward
Location: Line 262
- Jumps to any state in history
- Used by history visualization UI
- Restores state and updates index
Location: Line 242
- Clears entire history
- Resets all refs and state
- Used when loading new designs
Location: Line 301
- Retrieves state at specific index
- Returns
nullif index out of bounds
Location: Line 308
- Returns the current history state
- Helper for UI components
Location: Line 318
- Manually sets history (for loading saved designs)
- Used when restoring from saved designs
Location: Line 336
- Returns metadata about history position
- Helper for UI state management
Location: Line 84
- Generates JPEG thumbnail at 20% size
- Quality: 0.3 (compressed)
- Returns dataURL string
- Used for visual history grid
Location: Line 376
Purpose: Automatic integration with Fabric.js canvas
Features:
- Listens to
object:addedevent → callssave() - Listens to
object:modifiedevent → callssave() - Listens to
object:removedevent → callssave() - Initial save after 100ms delay
- Proper cleanup on unmount
Event Handlers:
handleObjectAdded() → undoRedo.save(canvas)
handleObjectModified() → undoRedo.save(canvas)
handleObjectRemoved() → undoRedo.save(canvas)Canvas integration and UI wiring
Location: Line 233-244
const undoRedo = useUndoRedoWithCanvas(fabricCanvasRef.current, {
maxHistory: 20,
throttleMs: 300,
includeThumbnail: true,
});
// Notify parent when ready
useEffect(() => {
if (onUndoRedoReady && undoRedo) {
onUndoRedoReady(undoRedo);
}
}, [onUndoRedoReady, undoRedo]);Location: Line 776-791
- Ctrl/Cmd + Z: Undo (line 777-782)
- Ctrl/Cmd + Y: Redo (line 785-790)
- Ctrl/Cmd + Shift + Z: Redo (line 785-790)
- Ctrl/Cmd + `: Toggle debug panel (line 792-793)
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
if (undoRedo.canUndo && fabricCanvasRef.current) {
undoRedo.undo(fabricCanvasRef.current);
}
}Location: Line 2248-2253
<BottomToolbar
canUndo={undoRedo.canUndo}
canRedo={undoRedo.canRedo}
onUndo={() => undoRedo.undo(fabricCanvasRef.current!)}
onRedo={() => undoRedo.redo(fabricCanvasRef.current!)}
historyLength={undoRedo.totalStates}
currentHistoryIndex={undoRedo.currentIndex + 1}
/>Location: Line 2159-2237
Toggle: Ctrl+` (backtick)
Features:
- Visual grid (10 columns) showing all history states
- Color coding:
- Blue = Current state
- Green = Past states (can undo to)
- Gray = Future states (can redo to)
- Detailed list with timestamps and JSON sizes
- Index and state information
History visualization UI in Layer drawer
Location: Line 16-17
undoRedo?: UseUndoRedoReturn;
canvas?: fabric.Canvas | null;Location: Line 116-261
Features:
-
Header with counter (line 119-148)
- Shows "History (current/total)"
- Undo/Redo buttons
-
Visual Grid (line 150-189)
- 10-column grid
- Shows up to 10 states (sampled from full history)
- Thumbnails with color-coded borders
- Click to jump to state
-
Detailed List (line 191-250)
- Shows last 5 states (including current)
- Thumbnail, timestamp, JSON size
- Current state highlighted
- Scrollable (max-height: 400px)
-
Empty State (line 252-259)
- Shown when no history exists
- Encourages user to start editing
Fallback: Layer list (line 263-401)
- Shown when
undoRedoprop is not available - Preserves original layer management functionality
Prop passing to LayerPanel
Location: Line 49
undoRedo?: any;Location: Line 2101-2103
<LayerPanel
objects={layers}
selectedId={selectedLayerId}
// ... other props
undoRedo={undoRedo}
canvas={canvas}
/>State management and prop orchestration
Location: Line 89
const [undoRedo, setUndoRedo] = useState<any>(null);Location: Line 128-130
const handleUndoRedoReady = useCallback((undoRedoData: any) => {
setUndoRedo(undoRedoData);
}, []);Location: Line 2427
<NewCanvasEditor
// ... other props
onUndoRedoReady={handleUndoRedoReady}
/>Location: Line 2411
<LeftSidebar
// ... other props
undoRedo={undoRedo}
/>Undo/Redo buttons in bottom toolbar
canUndo: boolean;
canRedo: boolean;
onUndo: () => void;
onRedo: () => void;
historyLength: number;
currentHistoryIndex: number;Location: Line ~248
{/* Undo Button */}
<button
onClick={onUndo}
disabled={!canUndo}
className={/* ... */}
title="Undo (Ctrl+Z)"
>
<UndoIcon />
</button>
{/* Redo Button */}
<button
onClick={onRedo}
disabled={!canRedo}
className={/* ... */}
title="Redo (Ctrl+Y)"
>
<RedoIcon />
</button>
{/* History Counter */}
<span className="text-xs">
Un})
</span>
Index}/{historyLengthdo ({currentHistory```
---
### 7. `/lib/hooks/useHistory.ts` (Legacy)
**Old 50-step system - kept for reference**
#### Key Differences:
- **Max History:** 50 (vs 20 in new system)
- **No thumbnail support**
- **No visual history UI**
- **Manual save required** (vs auto-save in new system)
- **Simpler state management** (no circular buffer)
#### Still Used For:
- `JSON_KEYS` constant (line 4-26)
- Properties to serialize: `name`, `gradientAngle`, `selectable`, etc.
- Font properties: `fontFamily`, `fontSize`, `fontWeight`, etc.
---
## Data Structures
### HistoryState Interface
```typescript
interface HistoryState {
timestamp: number; // Unix timestamp (ms)
canvasJSON: string; // Serialized canvas state
thumbnail?: string; // Optional JPEG dataURL
}interface UndoRedoState {
history: HistoryState[];
currentIndex: number;
canUndo: boolean;
canRedo: boolean;
totalStates: number;
}interface UseUndoRedoOptions {
maxHistory?: number;
throttleMs?: number;
includeThumbnail?: boolean;
}Canvas Object Change
↓
(object:added/modified/removed event)
↓
useUndoRedoWithCanvas
↓
undoRedo.save(canvas)
↓
Check throttle (300ms)
↓
Serialize canvas (toJSON)
↓
Create thumbnail (optional)
↓
Add to circular buffer
↓
Remove oldest if > max (20)
↓
Update state
↓
UI updates (buttons, counter)
- Max 20 states (configurable)
- When exceeded, oldest state is removed
- O(1) insert and access
- 300ms default throttle on saves
- Prevents excessive history entries during rapid changes
- Drawing operations: 100ms throttle
- Automatic on canvas changes
- No manual save calls needed
- Controlled by
useUndoRedoWithCanvas
- 20% size JPEG
- Quality: 0.3 (compressed)
- Used for visual history grid
- Falls back gracefully if generation fails
skipSaveRef- Prevents manual savesisRestoringRef- Prevents auto-saves during undo/redo- Prevents infinite loops and duplicate entries
- Integrated in Layer drawer
- 10-state visual grid
- 5-state detailed list
- Color-coded states
- Click to jump
| Shortcut | Action |
|---|---|
Ctrl/Cmd + Z |
Undo |
Ctrl/Cmd + Y |
Redo |
Ctrl/Cmd + Shift + Z |
Redo |
Ctrl/Cmd + ` |
Toggle debug panel |
const undoRedo = useUndoRedoWithCanvas(fabricCanvasRef.current, {
maxHistory: 20, // Max history steps
throttleMs: 300, // Throttle delay in ms
includeThumbnail: true // Generate thumbnails
});return canvas.toDataURL({
format: 'jpeg',
quality: 0.3, // Compression quality
multiplier: 0.2, // 20% size
});try {
// Save logic
} catch (error) {
console.error('Failed to save canvas state:', error);
}try {
// Undo/redo logic
} catch (error) {
console.error('Failed to undo/redo:', error);
skipSaveRef.current = false;
isRestoringRef.current = false;
}- Throttling: Prevents excessive saves
- Circular Buffer: O(1) operations
- Compressed Thumbnails: 20% size, quality 0.3
- Conditional Thumbnail: Optional via
includeThumbnail - Ref-based State: Avoids re-renders
- Event Cleanup: Proper listener removal
💾 [UndoRedo] Saved state X/Y↶ [UndoRedo] Undo to state X↷ [UndoRedo] Redo to state X🎯 [UndoRedo] Jumped to state X🗑️ [UndoRedo] History cleared📥 [UndoRedo] Loaded X states
- Toggle: Ctrl+`
- Location: Bottom of canvas
- Shows: Full history stack with details
| File | Lines | Purpose |
|---|---|---|
useUndoRedo.ts |
424 | Core hook implementation |
NewCanvasEditor.tsx |
~2300 | Integration & UI |
LayerPanel.tsx |
405 | History visualization |
LeftSidebar.tsx |
2142 | Prop orchestration |
NewEditorLayout.tsx |
~2450 | State management |
BottomToolbar.tsx |
~300 | Toolbar controls |
useHistory.ts |
125 | Legacy system |
Total: 7 files, ~7,000 lines
- 50-step history
- Manual save required
- No thumbnails
- No visual history
- Simple linear array
- 20-step circular buffer
- Auto-save on changes
- Thumbnail generation
- Visual history grid
- Debug panel
- Keyboard shortcuts
- Throttled saves
- Performance optimized
- Undo works (Ctrl+Z)
- Redo works (Ctrl+Y)
- Visual history updates
- Thumbnail generation
- Throttling prevents spam Layer
- drawer stays open
- Debug panel toggle (Ctrl+`)
- Jump to state works
- Circular buffer removes oldest
- Font restoration works
- Drawing operations tracked
- Error handling works
Solution: Added isRestoringRef flag (line 167, 207, 266)
- Prevents auto-save during state restoration
- Avoids loadFromJSON triggering object events
Solution: Removed onClick handlers (NewCanvasEditor.tsx line 2009, 2076)
- Drawer now stays open during canvas interaction
- Manual close still works via close button
- Customizable History Size - User-configurable max steps
- History Search - Search by timestamp or action type
- Branching History - Tree-like history for non-linear workflows
- State Comparison - Diff view between states
- Selective Undo - Undo specific object types
- History Export - Export history as JSON
- Preview Thumbnails - Hover to preview state
canvas.toJSON()- Serializationcanvas.loadFromJSON()- Restorationcanvas.on('object:*')- Change detection
saveCanvasState- Parent save (separate from history)editorSlice- Active tool state
The new undo/redo system provides:
- ✅ 20-step circular buffer
- ✅ Auto-save on changes
- ✅ Visual history in Layer drawer
- ✅ Keyboard shortcuts
- ✅ Debug panel
- ✅ Thumbnail previews
- ✅ Throttled performance
- ✅ Error handling
- ✅ Font restoration
- ✅ Circular buffer efficiency
Total Files: 7 Total Functions: 20+ Lines of Code: ~7,000 Status: ✅ Production Ready