Skip to content

Instantly share code, notes, and snippets.

@korrio
Created December 17, 2025 14:27
Show Gist options
  • Select an option

  • Save korrio/286c604090e52f7f6ac1a687780ee087 to your computer and use it in GitHub Desktop.

Select an option

Save korrio/286c604090e52f7f6ac1a687780ee087 to your computer and use it in GitHub Desktop.
undo-redo-miracle.md

Undo/Redo System Documentation

Overview

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.


Core Files

1. /lib/hooks/useUndoRedo.ts

Primary hook implementing the undo/redo system with circular buffer

Main Function: useUndoRedo(options)

Location: Line 57

Parameters:

  • maxHistory (number, default: 20) - Maximum history steps
  • throttleMs (number, default: 300) - Throttle delay for save operations
  • includeThumbnail (boolean, default: true) - Whether to generate thumbnails

Returns: UseUndoRedoReturn object with state and methods

State Properties:

  • history: HistoryState[] - Array of history states
  • currentIndex: number - Current position in history (-1 = empty)
  • canUndo: boolean - Whether undo is possible
  • canRedo: boolean - Whether redo is possible
  • totalStates: number - Total number of states saved

Core Methods:

save(canvas, skip?)

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 skipSaveRef and isRestoringRef to prevent recursive saves
undo(canvas, callback?)

Location: Line 162

  • Restores previous state
  • Sets isRestoringRef.current = true to prevent auto-save during restoration
  • Uses canvas.loadFromJSON() to restore state
  • Updates currentIndexRef and state
  • Calls optional callback after completion
redo(canvas, callback?)

Location: Line 202

  • Restores next state (if available)
  • Same mechanism as undo() but moves forward
jumpTo(index, canvas, callback?)

Location: Line 262

  • Jumps to any state in history
  • Used by history visualization UI
  • Restores state and updates index
clear()

Location: Line 242

  • Clears entire history
  • Resets all refs and state
  • Used when loading new designs
getStateAt(index)

Location: Line 301

  • Retrieves state at specific index
  • Returns null if index out of bounds
getCurrentState()

Location: Line 308

  • Returns the current history state
  • Helper for UI components
setHistory(states, index)

Location: Line 318

  • Manually sets history (for loading saved designs)
  • Used when restoring from saved designs
getHistoryMetadata()

Location: Line 336

  • Returns metadata about history position
  • Helper for UI state management

Helper Functions:

createThumbnail(canvas)

Location: Line 84

  • Generates JPEG thumbnail at 20% size
  • Quality: 0.3 (compressed)
  • Returns dataURL string
  • Used for visual history grid

Secondary Hook: useUndoRedoWithCanvas(canvas, options)

Location: Line 376

Purpose: Automatic integration with Fabric.js canvas

Features:

  • Listens to object:added event → calls save()
  • Listens to object:modified event → calls save()
  • Listens to object:removed event → calls save()
  • Initial save after 100ms delay
  • Proper cleanup on unmount

Event Handlers:

handleObjectAdded()  undoRedo.save(canvas)
handleObjectModified()  undoRedo.save(canvas)
handleObjectRemoved()  undoRedo.save(canvas)

2. /components/editor/NewCanvasEditor.tsx

Canvas integration and UI wiring

Initialization

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]);

Keyboard Shortcuts

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);
  }
}

Props Passed to BottomToolbar

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}
/>

Debug Panel

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

3. /components/editor/LayerPanel.tsx

History visualization UI in Layer drawer

Props

Location: Line 16-17

undoRedo?: UseUndoRedoReturn;
canvas?: fabric.Canvas | null;

History Visualization Section

Location: Line 116-261

Features:

  1. Header with counter (line 119-148)

    • Shows "History (current/total)"
    • Undo/Redo buttons
  2. 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
  3. Detailed List (line 191-250)

    • Shows last 5 states (including current)
    • Thumbnail, timestamp, JSON size
    • Current state highlighted
    • Scrollable (max-height: 400px)
  4. Empty State (line 252-259)

    • Shown when no history exists
    • Encourages user to start editing

Fallback: Layer list (line 263-401)

  • Shown when undoRedo prop is not available
  • Preserves original layer management functionality

4. /components/editor/LeftSidebar.tsx

Prop passing to LayerPanel

Props Interface

Location: Line 49

undoRedo?: any;

Component Usage

Location: Line 2101-2103

<LayerPanel
  objects={layers}
  selectedId={selectedLayerId}
  // ... other props
  undoRedo={undoRedo}
  canvas={canvas}
/>

5. /components/editor/NewEditorLayout.tsx

State management and prop orchestration

State

Location: Line 89

const [undoRedo, setUndoRedo] = useState<any>(null);

Callback Handler

Location: Line 128-130

const handleUndoRedoReady = useCallback((undoRedoData: any) => {
  setUndoRedo(undoRedoData);
}, []);

Passing to NewCanvasEditor

Location: Line 2427

<NewCanvasEditor
  // ... other props
  onUndoRedoReady={handleUndoRedoReady}
/>

Passing to LeftSidebar

Location: Line 2411

<LeftSidebar
  // ... other props
  undoRedo={undoRedo}
/>

6. /components/editor/BottomToolbar.tsx

Undo/Redo buttons in bottom toolbar

Props

canUndo: boolean;
canRedo: boolean;
onUndo: () => void;
onRedo: () => void;
historyLength: number;
currentHistoryIndex: number;

Usage

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
}

UndoRedoState Interface

interface UndoRedoState {
  history: HistoryState[];
  currentIndex: number;
  canUndo: boolean;
  canRedo: boolean;
  totalStates: number;
}

UseUndoRedoOptions Interface

interface UseUndoRedoOptions {
  maxHistory?: number;
  throttleMs?: number;
  includeThumbnail?: boolean;
}

Flow Diagram

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)

Key Features

1. Circular Buffer

  • Max 20 states (configurable)
  • When exceeded, oldest state is removed
  • O(1) insert and access

2. Throttling

  • 300ms default throttle on saves
  • Prevents excessive history entries during rapid changes
  • Drawing operations: 100ms throttle

3. Auto-Save

  • Automatic on canvas changes
  • No manual save calls needed
  • Controlled by useUndoRedoWithCanvas

4. Thumbnail Generation

  • 20% size JPEG
  • Quality: 0.3 (compressed)
  • Used for visual history grid
  • Falls back gracefully if generation fails

5. State Restoration Flags

  • skipSaveRef - Prevents manual saves
  • isRestoringRef - Prevents auto-saves during undo/redo
  • Prevents infinite loops and duplicate entries

6. Visual History

  • Integrated in Layer drawer
  • 10-state visual grid
  • 5-state detailed list
  • Color-coded states
  • Click to jump

Keyboard Shortcuts

Shortcut Action
Ctrl/Cmd + Z Undo
Ctrl/Cmd + Y Redo
Ctrl/Cmd + Shift + Z Redo
Ctrl/Cmd + ` Toggle debug panel

Configuration

In NewCanvasEditor.tsx (Line 233-237)

const undoRedo = useUndoRedoWithCanvas(fabricCanvasRef.current, {
  maxHistory: 20,        // Max history steps
  throttleMs: 300,       // Throttle delay in ms
  includeThumbnail: true // Generate thumbnails
});

Thumbnail Settings (Line 88-92)

return canvas.toDataURL({
  format: 'jpeg',
  quality: 0.3,      // Compression quality
  multiplier: 0.2,   // 20% size
});

Error Handling

Save Errors (Line 154-156)

try {
  // Save logic
} catch (error) {
  console.error('Failed to save canvas state:', error);
}

Undo/Redo Errors (Line 192-196, 232-236)

try {
  // Undo/redo logic
} catch (error) {
  console.error('Failed to undo/redo:', error);
  skipSaveRef.current = false;
  isRestoringRef.current = false;
}

Performance Optimizations

  1. Throttling: Prevents excessive saves
  2. Circular Buffer: O(1) operations
  3. Compressed Thumbnails: 20% size, quality 0.3
  4. Conditional Thumbnail: Optional via includeThumbnail
  5. Ref-based State: Avoids re-renders
  6. Event Cleanup: Proper listener removal

Debugging

Console Logs

  • 💾 [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

Debug Panel

  • Toggle: Ctrl+`
  • Location: Bottom of canvas
  • Shows: Full history stack with details

File Count Summary

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


Evolution

Old System (useHistory.ts)

  • 50-step history
  • Manual save required
  • No thumbnails
  • No visual history
  • Simple linear array

New System (useUndoRedo.ts)

  • 20-step circular buffer
  • Auto-save on changes
  • Thumbnail generation
  • Visual history grid
  • Debug panel
  • Keyboard shortcuts
  • Throttled saves
  • Performance optimized

Testing Checklist

  • 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

Known Issues & Solutions

Issue: "Could only undo 1 step"

Solution: Added isRestoringRef flag (line 167, 207, 266)

  • Prevents auto-save during state restoration
  • Avoids loadFromJSON triggering object events

Issue: Drawer closing on canvas click

Solution: Removed onClick handlers (NewCanvasEditor.tsx line 2009, 2076)

  • Drawer now stays open during canvas interaction
  • Manual close still works via close button

Future Enhancements

  1. Customizable History Size - User-configurable max steps
  2. History Search - Search by timestamp or action type
  3. Branching History - Tree-like history for non-linear workflows
  4. State Comparison - Diff view between states
  5. Selective Undo - Undo specific object types
  6. History Export - Export history as JSON
  7. Preview Thumbnails - Hover to preview state

Related Components

Fabric.js Integration

  • canvas.toJSON() - Serialization
  • canvas.loadFromJSON() - Restoration
  • canvas.on('object:*') - Change detection

Redux Integration

  • saveCanvasState - Parent save (separate from history)
  • editorSlice - Active tool state

Conclusion

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment