How DrawDB Tracks History for Table and Field Updates: Inside the Undo/Redo System

DrawDB tracks history for table and field updates using a client-side undo/redo system built on two stacks (undo and redo) and a minimal diff engine that captures only the changes between diagram snapshots.

DrawDB is an open-source database diagramming tool that provides robust version control for schema modifications directly in the browser. Understanding how DrawDB tracks history for table and field updates reveals a lightweight yet powerful architecture that enables seamless undo and redo functionality across the entire diagram editor. The implementation relies on React hooks and context providers to maintain immutable state snapshots without server dependencies.

The Core Architecture: Stacks and Diffs

At the heart of DrawDB's history tracking lies a dual-stack system implemented in src/hooks/useUndoRedo.js. This hook maintains an undo stack for past actions, a redo stack for reversed actions, and a pointer to the present state representing the current diagram.

The UndoRedoContext provider in src/context/UndoRedoContext.jsx wraps the application, exposing history functions to any component that modifies tables or fields. When a user renames a column or moves a table, the component calls addHistoryEntry() with the new diagram state.

To minimize memory footprint, DrawDB does not store complete state snapshots. Instead, the diff utility in src/utils/diff.js generates a minimal diff object between the previous and new states. This diff represents precisely what changed—whether a field type update, table deletion, or relationship modification—making history entries compact and fast to apply.

Creating History Entries with addHistoryEntry

When a table or field update occurs, the application invokes addHistoryEntry(newState) to record the change. This function calculates the difference between the current and new states, pushes that diff onto the undo stack via a reducer dispatch, and clears the redo stack since a new action invalidates the forward history.

function addHistoryEntry(newState) {
  const diffEntry = createDiff(presentState, newState);   // src/utils/diff.js
  dispatch({ type: "PUSH", payload: diffEntry });        // pushes onto undo stack
  setPresentState(newState);                             // updates the current diagram
}

Components throughout the editor integrate this hook to track modifications. For example, when renaming a column:

import { useUndoRedo } from '@/hooks/useUndoRedo';

function onColumnRename(tableId, columnId, newName) {
  const { diagram, addHistoryEntry } = useUndoRedo();

  const updatedDiagram = updateColumn(diagram, tableId, columnId, { name: newName });
  addHistoryEntry(updatedDiagram);           // <-- history recorded
}

Executing Undo and Redo Operations

The undo operation reverses the most recent change by popping the latest diff from the undo stack, applying its inverse transformation using diff.applyInverse, and moving that diff to the redo stack. The redo operation performs the inverse, re-applying a stored diff to restore a previously undone state.

import { useUndoRedo } from '@/hooks/useUndoRedo';

function handleUndo() {
  const { undo } = useUndoRedo();
  undo();                                    // reverts the most recent diff
}

For redo functionality:

import { useUndoRedo } from '@/hooks/useUndoRedo';

function handleRedo() {
  const { redo } = useUndoRedo();
  redo();                                    // reapplies the last undone diff
}

The system also provides clearHistory() to reset both stacks when loading a new diagram or performing bulk operations that should not be tracked.

Persistence and Session Recovery

Because the history consists of immutable diff objects, DrawDB can serialize the entire undo/redo stack for storage. The SaveStateContext provider in src/context/SaveStateContext.jsx persists these stacks to localStorage, allowing users to restore their session—including the complete history of table and field updates—after closing and reopening the browser.

This architecture also supports export and import functionality. Users can serialize their diagram along with its history, enabling version control workflows where previous schema iterations remain accessible even after file transfers.

Summary

  • DrawDB implements history tracking through a custom useUndoRedo hook in src/hooks/useUndoRedo.js that maintains separate undo and redo stacks.
  • The system uses a minimal diff engine in src/utils/diff.js to record changes between diagram states rather than storing full snapshots.
  • Components call addHistoryEntry() to record table and field modifications, pushing compressed diff objects onto the history stack.
  • Undo operations apply inverse diffs using diff.applyInverse while redo operations re-apply stored diffs, enabling arbitrary navigation through the edit history.
  • History persistence is handled by SaveStateContext, which stores the stack state in localStorage for session recovery and supports serialization for export/import workflows.

Frequently Asked Questions

How does DrawDB store history without a backend database?

DrawDB uses a client-side architecture where the useUndoRedo hook maintains two in-memory stacks containing minimal diff objects. These diffs represent only the changes between states rather than complete diagram copies. The SaveStateContext provider serializes these stacks to localStorage, enabling history persistence across page reloads without requiring server-side storage.

What happens to the redo stack when I make a new edit after undoing?

When you perform a new action after undoing, the addHistoryEntry() function automatically clears the redo stack. This behavior follows standard undo/redo conventions: once you diverge from the historical timeline by creating new changes, the previously undone actions are permanently discarded from the forward history to prevent branching state conflicts.

Can DrawDB history track individual field properties like data types or constraints?

Yes, the diff engine in src/utils/diff.js captures granular changes at the field level. When you modify a column's data type, name, or constraints, the system generates a diff object representing that specific property change. The updateColumn utility creates a new diagram state with the modified field, and addHistoryEntry() records the precise difference between the old and new column definitions.

Is there a limit to how many undo steps DrawDB retains?

According to the source implementation in src/hooks/useUndoRedo.js, there is no explicit hard limit on the undo stack size. Because each entry stores only a minimal diff rather than a full state snapshot, the memory footprint remains lightweight even with extensive histories. The system can theoretically track hundreds of table and field updates while maintaining responsive performance, limited only by available browser memory.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →