How DrawDB Implements Undo and Redo Functionality: A Deep Dive into the Command-Stack Pattern
DrawDB uses a lightweight command-stack pattern powered by React's useState hooks in UndoRedoContext, storing immutable diagram snapshots in two arrays—undoStack and redoStack—that components manipulate via the useUndoRedo hook.
The open-source database diagramming tool DrawDB (available at github.com/drawdb-io/drawdb) implements its entire undo/redo system without external dependencies. Instead, it relies on a pure-React context provider that maintains two simple JavaScript arrays. This design keeps the codebase lean while providing reliable history navigation for complex diagram operations.
The Core Architecture: UndoRedoContext
At the heart of DrawDB's undo/redo functionality lies src/context/UndoRedoContext.jsx. This file defines a React context that exposes four critical pieces of state:
undoStack– An array storing previous diagram states (snapshots) in chronological orderredoStack– An array storing states that have been undone and can be reappliedsetUndoStack– State setter for pushing new snapshots onto the undo historysetRedoStack– State setter for managing the redo branch
The context provider wraps these in a standard React state pattern using useState hooks, making the entire history system reactive to updates.
Consuming the Context with useUndoRedo
Rather than importing the context directly, components throughout DrawDB use a dedicated hook defined in src/hooks/useUndoRedo.js. This hook returns the full context value, allowing any component to read stack lengths or trigger history operations.
import useUndoRedo from "@/hooks/useUndoRedo";
function MyComponent() {
const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo();
// Component can now check history depth or push new states
}
How Diagram Mutations Push to the Undo Stack
When a user performs any mutable action—creating a table, dragging a node, editing a column, or deleting a relationship—the executing component captures the current diagram state as an immutable snapshot and pushes it onto undoStack. Simultaneously, it clears redoStack because a new branch of history invalidates any previously undone actions.
function handleAddTable(newTable) {
// 1️⃣ Capture current diagram state before mutation
const snapshot = captureDiagramState(); // Returns JSON-serializable diagram object
// 2️⃣ Push onto undo stack and clear redo stack (new branch)
setUndoStack((prev) => [...prev, snapshot]);
setRedoStack([]);
// 3️⃣ Execute the actual mutation
addTableToDiagram(newTable);
}
The setUndoStack((prev) => [...prev, snapshot]) pattern ensures immutable updates, which is critical for React's reconciliation and for maintaining clean history without reference collisions.
Executing Undo Operations
When the user triggers an undo—via toolbar button, keyboard shortcut (Ctrl/Cmd+Z), or the timeline interface—the system pops the most recent snapshot from undoStack, restores the diagram to that state, and pushes the popped entry onto redoStack.
function undo() {
setUndoStack((undoPrev) => {
if (undoPrev.length === 0) return undoPrev; // Guard: nothing to undo
const lastSnapshot = undoPrev[undoPrev.length - 1]; // State to restore
// Push onto redo stack for potential reapplication
setRedoStack((redoPrev) => [...redoPrev, lastSnapshot]);
// Restore diagram from snapshot (implementation specific to DrawDB's state management)
restoreDiagramState(lastSnapshot);
// Remove from undo stack
return undoPrev.slice(0, -1);
});
}
Executing Redo Operations
The redo operation mirrors undo in reverse. It pops from redoStack, restores that snapshot, and pushes it back onto undoStack so the action can be undone again if needed.
function redo() {
setRedoStack((redoPrev) => {
if (redoPrev.length === 0) return redoPrev; // Guard: nothing to redo
const nextSnapshot = redoPrev[redoPrev.length - 1]; // State to reapply
// Push back onto undo stack
setUndoStack((undoPrev) => [...undoPrev, nextSnapshot]);
// Restore diagram from snapshot
restoreDiagramState(nextSnapshot);
// Remove from redo stack
return redoPrev.slice(0, -1);
});
}
UI Integration: ControlPanel and Timeline Components
The undo/redo functionality surfaces in DrawDB's interface through two key components:
-
src/components/EditorHeader/ControlPanel.jsx– Contains the main toolbar buttons for undo/redo. These buttons readundoStack.lengthandredoStack.lengthfrom the context to determine their enabled/disabled state, and invoke the handlers described above on click. -
src/components/EditorHeader/SideSheet/Timeline.jsx– Provides a visual history timeline that displays the entireundoStackas a navigable list. Users can jump to any previous state by selecting an entry from this timeline, which effectively performs multiple undo operations (or a direct state restoration) in sequence.
Both components consume the same useUndoRedo hook, ensuring consistent stack state across the entire application.
Why Snapshots Over Command Objects
DrawDB's implementation uses full state snapshots (complete diagram JSON objects) rather than granular command objects with execute() and unexecute() methods. This design choice offers several advantages:
- Simplicity: No complex command classes or inheritance hierarchies
- Reliability: Restoring a complete state eliminates risk of partially applied operations
- Debugging: Each snapshot is inspectable as plain JSON
- Timeline navigation: Jumping to arbitrary history points is trivial—just restore that specific snapshot
The trade-off is memory consumption for large diagrams with extensive history, though in practice DrawDB's diagram sizes remain manageable for browser memory.
Summary
-
UndoRedoContextinsrc/context/UndoRedoContext.jsxholds twouseStatearrays:undoStackandredoStack -
The
useUndoRedohook insrc/hooks/useUndoRedo.jsprovides the standard interface for components to access history state -
Mutating operations push immutable diagram snapshots onto
undoStackand clearredoStackto start fresh branches -
Undo pops from
undoStack, restores that snapshot, and pushes ontoredoStack -
Redo pops from
redoStack, restores that snapshot, and pushes back ontoundoStack -
UI controls in
ControlPanel.jsxandTimeline.jsxread stack lengths for state and invoke these operations
Frequently Asked Questions
How does DrawDB handle memory usage with large undo histories?
DrawDB stores complete JSON snapshots of diagram state. For typical database schemas, these objects remain small enough that browsers handle dozens of history entries without issues. The application does not currently implement history depth limits, though the snapshot-based approach makes adding such limits straightforward by truncating undoStack when it exceeds a threshold.
Can users navigate to arbitrary points in history, or only step by step?
Users can jump to any previous state through the Timeline component in src/components/EditorHeader/SideSheet/Timeline.jsx. This interface renders the entire undoStack as a clickable list. Selecting an entry restores that specific snapshot, effectively performing a direct state restoration rather than sequential undo operations.
Why doesn't DrawDB use a dedicated undo/redo library?
The snapshot-based approach in UndoRedoContext provides sufficient functionality without adding dependencies. For a diagramming tool where state restorations need to be instantaneous and complete, immutable snapshots are simpler than command-pattern libraries. This keeps the bundle size smaller and the behavior fully transparent in the source code.
What happens to the redo stack when a new action is performed?
Any new mutable action automatically clears redoStack via setRedoStack([]). This follows standard undo/redo conventions: once you branch forward with new work, previously undone actions are no longer reachable. The setRedoStack([]) call occurs immediately after pushing the pre-action snapshot onto undoStack.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →