How DrawDB Implements Undo/Redo Functionality in the Diagram Editor
DrawDB manages diagram editing history using a command-pattern approach with dual state stacks wrapped in a custom React hook and context provider.
DrawDB is an open-source database entity-relationship diagram editor that provides robust undo/redo functionality to ensure users can safely experiment with database designs. The implementation relies on a centralized state management system that captures snapshots of the entire diagram after every mutation. This article examines the architecture, key source files, and practical integration patterns that make the feature work seamlessly across the application.
The Command Pattern Architecture
The undo/redo system follows the classic command pattern by maintaining two distinct history stacks. This design ensures linear edit history and prevents complex branching states that could confuse users.
State History Stacks
At the core of the system are two arrays:
- Undo stack: Stores previous diagram states, allowing users to step backward through their editing history.
- Redo stack: Stores states that have been undone, enabling users to step forward again.
When a user performs an action—such as adding a table, moving an element, or modifying a relationship—the current diagram state is pushed onto the undo stack. Simultaneously, the redo stack is cleared to maintain a linear history. This behavior matches user expectations in modern design applications.
State Snapshot Management
The system exposes four primary functions through the context:
undo(): Pops the latest state from the undo stack, pushes it to the redo stack, and restores the editor to that previous state.redo(): Pops the latest state from the redo stack, pushes it back onto the undo stack, and updates the diagram accordingly.addState(state): Captures a new diagram snapshot and adds it to the undo stack. Called after every mutation.clearHistory(): Resets both stacks, typically invoked when loading a new diagram file.
Core Implementation Files
useUndoRedo Hook
The business logic resides in src/hooks/useUndoRedo.js. This custom React hook maintains the dual stack structure using React state or refs and returns the four core functions along with boolean flags indicating whether undo or redo actions are available.
UndoRedoContext Provider
To make the undo/redo API accessible throughout the component tree without prop drilling, DrawDB wraps the application in UndoRedoContext defined in src/context/UndoRedoContext.jsx. This context provider instantiates the hook and passes its return values to all child components.
Workspace Integration
The context is provided at the top level of the editor in src/components/Workspace.jsx. This ensures that every diagram-related component—from the canvas to the property panels—can access the undo/redo functions via the useContext hook.
Keyboard Shortcuts and Mutation Tracking
useDiagram Hook Integration
All diagram mutations are funneled through src/hooks/useDiagram.js, which serves as the primary interface for modifying diagram state. After every mutation (adding tables, updating relationships, repositioning elements), this hook calls addState() to capture the new state.
This tight coupling ensures the history stack remains synchronized with the visual representation on screen.
Keyboard Event Handling
Keyboard shortcuts are registered within the same useDiagram.js file. The system listens for:
- Ctrl + Z: Triggers
undo() - Ctrl + Shift + Z: Triggers
redo()
These event listeners prevent default browser behavior and call the appropriate context functions, providing the standard editing experience users expect from desktop design applications.
Practical Usage Example
Components consume the undo/redo functionality through React context. Below is a typical implementation pattern for a toolbar component that both triggers mutations and provides undo/redo buttons:
import { useContext } from 'react';
import { UndoRedoContext } from '../context/UndoRedoContext';
import { DiagramContext } from '../context/DiagramContext';
function ToolbarButtons() {
const { undo, redo, addState } = useContext(UndoRedoContext);
const { diagram, setDiagram } = useContext(DiagramContext);
const addTable = () => {
const newTable = createEmptyTable();
const newDiagram = {
...diagram,
tables: [...diagram.tables, newTable]
};
setDiagram(newDiagram);
addState(newDiagram);
};
return (
<div className="toolbar">
<button onClick={undo}>Undo (Ctrl+Z)</button>
<button onClick={redo}>Redo (Ctrl+Shift+Z)</button>
<button onClick={addTable}>Add Table</button>
</div>
);
}
The keyboard shortcut registration typically appears in the main diagram hook:
// Inside src/hooks/useDiagram.js
useEffect(() => {
const handleKeyDown = (e) => {
if (e.ctrlKey && !e.shiftKey && e.key === 'z') {
e.preventDefault();
undo();
}
if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
e.preventDefault();
redo();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [undo, redo]);
Summary
- DrawDB implements undo/redo functionality using a command-pattern architecture with dual state stacks maintained in
src/hooks/useUndoRedo.js. - The
UndoRedoContextinsrc/context/UndoRedoContext.jsxexposesundo(),redo(),addState(), andclearHistory()to the entire application. - All diagram mutations flow through
src/hooks/useDiagram.js, which automatically captures state snapshots after every change. - Keyboard shortcuts (Ctrl+Z for undo, Ctrl+Shift+Z for redo) are handled in
useDiagram.jsand integrated with the context methods. - The context provider wraps the editor in
src/components/Workspace.jsx, ensuring consistent history management across all diagram editing actions.
Frequently Asked Questions
How does DrawDB store undo history?
DrawDB stores undo history using two in-memory arrays (stacks) maintained by the useUndoRedo hook. The undo stack holds previous diagram states, while the redo stack holds states that have been undone. When the user performs a new action, the current state is pushed onto the undo stack and the redo stack is cleared.
What happens when I press Ctrl+Z in DrawDB?
When you press Ctrl+Z, the keyboard event handler in src/hooks/useDiagram.js intercepts the shortcut and calls the undo() function from UndoRedoContext. This pops the most recent state from the undo stack, pushes it onto the redo stack, and restores the diagram to that previous configuration.
Can I redo an action after undoing in DrawDB?
Yes. DrawDB supports redo via Ctrl+Shift+Z. When triggered, the redo() function pops the state from the redo stack and pushes it back onto the undo stack, effectively moving forward in the edit history. The redo stack is only available until you perform a new mutation, which clears it.
Where is the undo/redo logic defined in the DrawDB codebase?
The core logic resides in src/hooks/useUndoRedo.js, while the React context provider is located in src/context/UndoRedoContext.jsx. The integration with the diagram editor happens in src/components/Workspace.jsx (provider placement) and src/hooks/useDiagram.js (mutation tracking and keyboard shortcuts).
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 →