# How drawDB Implements Undo/Redo Functionality for Diagram Edits

> Discover how drawDB implements robust undo redo functionality for diagram edits. Learn about its stack based history system and immutable action objects for seamless state traversal.

- Repository: [drawDB/drawdb](https://github.com/drawdb-io/drawdb)
- Tags: internals
- Published: 2026-08-14

---

**DrawDB uses a stack-based history system where every diagram edit is recorded as an immutable action object containing both forward and reverse payloads, enabling seamless traversal through editing states.**

The open-source diagram editor drawDB manages editing history through a **React context-driven architecture** centered on two arrays: an `undoStack` and a `redoStack`. This design allows users to reverse and replay any change—from adding tables to moving fields—with predictable behavior. Let's examine how the implementation works in the actual source code.

## Core Architecture: UndoRedoContext and the Stack Model

### UndoRedoContext: Global History Storage

The foundation lives in [`src/context/UndoRedoContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/UndoRedoContext.jsx). This React context provides:

- `undoStack` — array of completed actions
- `redoStack` — array of reversed actions waiting to be restored
- `setUndoStack` and `setRedoStack` — state setters for mutation

```jsx
// src/context/UndoRedoContext.jsx
const UndoRedoContext = createContext();

export function UndoRedoProvider({ children }) {
  const [undoStack, setUndoStack] = useState([]);
  const [redoStack, setRedoStack] = useState([]);

  return (
    <UndoRedoContext.Provider
      value={{ undoStack, redoStack, setUndoStack, setRedoStack }}
    >
      {children}
    </UndoRedoContext.Provider>
  );
}

```

Components access this via `useUndoRedo()` from [`src/hooks/useUndoRedo.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useUndoRedo.js), a thin wrapper that avoids direct context imports:

```javascript
// src/hooks/useUndoRedo.js
import { useContext } from 'react';
import { UndoRedoContext } from '../context/UndoRedoContext';

export const useUndoRedo = () => useContext(UndoRedoContext);

```

### Action and ObjectType Enums

Every recorded change follows a strict schema defined in [`src/data/constants.js`](https://github.com/drawdb-io/drawdb/blob/main/src/data/constants.js). The **Action** enum specifies operation types, while **ObjectType** identifies diagram elements:

```javascript
// src/data/constants.js (excerpt)
export const Action = {
  ADD: 'ADD',
  MOVE: 'MOVE',
  DELETE: 'DELETE',
  EDIT: 'EDIT',
};

export const ObjectType = {
  TABLE: 'TABLE',
  AREA: 'AREA',
  NOTE: 'NOTE',
  RELATIONSHIP: 'RELATIONSHIP',
  TYPE: 'TYPE',
  ENUM: 'ENUM',
  DBML: 'DBML',
};

```

These enums ensure consistent payload shapes across the entire codebase.

## How Actions Are Recorded

When any component modifies the diagram, it pushes an **action object** onto `undoStack` and clears `redoStack` (new branch in history). The standard action shape is:

```javascript
{
  action: Action.ADD | Action.MOVE | Action.DELETE | Action.EDIT,
  element: ObjectType.TABLE | ObjectType.AREA | ...,
  id: <elementIdentifier>,           // table ID, note ID, etc.
  data: <forwardPayload>,            // data to re-apply (for redo)
  undo: <reversePayload>,            // data to revert (for undo)
  bulk: <boolean>,                   // true = batch operation
  elements: [<subActions>],          // used when bulk is true
  // element-specific fields: x, y, tid, fid, rid, aid, nid, etc.
}

```

### Recording Example: Adding a Table

Components that add tables push actions like this:

```javascript
const { setUndoStack } = useUndoRedo();

const addNewTable = (table) => {
  // ... actual table creation logic ...
  
  setUndoStack(prev => [
    ...prev,
    {
      action: Action.ADD,
      element: ObjectType.TABLE,
      data: table,   // full table definition for redo
    },
  ]);
};

```

### Recording Example: Moving a Table

Moves store the **previous position** as the undo payload:

```javascript
const moveTable = (id, newX, newY) => {
  const table = tables.find(t => t.id === id);
  const oldPos = { x: table.x, y: table.y };

  updateTable(id, { x: newX, y: newY });

  setUndoStack(prev => [
    ...prev,
    {
      action: Action.MOVE,
      element: ObjectType.TABLE,
      id,
      x: oldPos.x,   // stored for undo
      y: oldPos.y,
    },
  ]);
};

```

## Undo Implementation in ControlPanel.jsx

The actual undo logic resides in [`src/components/EditorHeader/ControlPanel.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/components/EditorHeader/ControlPanel.jsx). This function inspects the last action, performs the inverse operation, and transfers the action to `redoStack`.

```javascript
const undo = () => {
  if (undoStack.length === 0) return;
  const a = undoStack[undoStack.length - 1];
  
  // Remove from undo stack
  setUndoStack(prev => prev.filter((_, i) => i !== prev.length - 1));

  // Handle bulk operations (multiple elements changed at once)
  if (a.bulk) {
    for (const element of a.elements) {
      if (element.type === ObjectType.TABLE) {
        updateTable(element.id, element.undo);
      } else if (element.type === ObjectType.AREA) {
        updateArea(element.id, element.undo);
      } else if (element.type === ObjectType.NOTE) {
        updateNote(element.id, element.undo);
      }
    }
    setRedoStack(prev => [...prev, a]);
    return;
  }

  // Handle DBML snapshot swaps
  if (a.element === ObjectType.DBML) {
    setRedoStack(prev => [...prev, swapDbmlSnapshot(a)]);
    return;
  }

  // Simple actions: ADD, MOVE, DELETE, EDIT
  if (a.action === Action.ADD) {
    // Undo addition → delete the created element
    if (a.element === ObjectType.TABLE) {
      deleteTable(a.data.table.id, false);
    } else if (a.element === ObjectType.AREA) {
      deleteArea(areas[areas.length - 1].id, false);
    } else if (a.element === ObjectType.NOTE) {
      deleteNote(notes[notes.length - 1].id, false);
    } else if (a.element === ObjectType.RELATIONSHIP) {
      deleteRelationship(a.data.relationship.id, false);
    }
    // ... TYPE, ENUM handling ...
    setRedoStack(prev => [...prev, a]);
    
  } else if (a.action === Action.MOVE) {
    // Undo move → restore previous coordinates
    if (a.element === ObjectType.TABLE) {
      const { x, y } = tables.find(t => t.id === a.id);
      setRedoStack(prev => [...prev, { ...a, x, y }]);
      updateTable(a.id, { x: a.x, y: a.y });
    }
    // ... AREA, NOTE handling ...
    
  } else if (a.action === Action.DELETE) {
    // Undo deletion → re-add the removed element
    if (a.element === ObjectType.TABLE) {
      a.data.relationship.forEach(r => addRelationship(r, false));
      addTable(a.data, false);
    }
    // ... other element types ...
    setRedoStack(prev => [...prev, a]);
    
  } else if (a.action === Action.EDIT) {
    // Undo edit → apply stored reverse payload
    if (a.element === ObjectType.TABLE) {
      if (a.component === "field") {
        updateField(a.tid, a.fid, a.undo);
      }
      // ... other components: field_add, index, unique_constraint, etc.
    } else if (a.element === ObjectType.RELATIONSHIP) {
      updateRelationship(a.rid, a.undo);
    }
    // ... AREA, NOTE, TYPE, ENUM handling ...
    setRedoStack(prev => [...prev, a]);
  }
};

```

Key implementation details:

- **`false` parameter** — prevents creating new undo entries during reversal (avoiding infinite loops)
- **Coordinate preservation** — moves capture current position before restoring old position, enabling redo
- **Relationship handling** — table deletions store associated relationships, which are restored together

## Redo Implementation: Mirror of Undo

The **redo** function in the same file applies forward operations from `redoStack` back onto `undoStack`:

```javascript
const redo = () => {
  if (redoStack.length === 0) return;
  const a = redoStack[redoStack.length - 1];
  
  setRedoStack(prev => prev.filter((_, i) => i !== prev.length - 1));

  if (a.bulk) {
    a.elements.forEach(el => {
      if (el.type === ObjectType.TABLE) addTable(el.data, false);
      if (el.type === ObjectType.AREA) addArea(el.data, false);
      if (el.type === ObjectType.NOTE) addNote(el.data, false);
    });
    setUndoStack(prev => [...prev, a]);
    return;
  }

  if (a.element === ObjectType.DBML) {
    setUndoStack(prev => [...prev, swapDbmlSnapshot(a)]);
    return;
  }

  if (a.action === Action.ADD) {
    if (a.element === ObjectType.TABLE) addTable(a.data, false);
    if (a.element === ObjectType.AREA) addArea(a.data, false);
    if (a.element === ObjectType.NOTE) addNote(a.data, false);
    setUndoStack(prev => [...prev, a]);
    
  } else if (a.action === Action.MOVE) {
    if (a.element === ObjectType.TABLE) {
      updateTable(a.id, { x: a.x, y: a.y });
    }
    // ... AREA, NOTE ...
    setUndoStack(prev => [...prev, a]);
    
  } else if (a.action === Action.DELETE) {
    if (a.element === ObjectType.TABLE) deleteTable(a.data.id, false);
    setUndoStack(prev => [...prev, a]);
    
  } else if (a.action === Action.EDIT) {
    if (a.element === ObjectType.TABLE) {
      if (a.component === "field") updateField(a.tid, a.fid, a.data);
    }
    setUndoStack(prev => [...prev, a]);
  }
};

```

Notice the symmetry: **undo uses `a.undo`, redo uses `a.data`** (or `a.x, a.y` for moves).

## Integration with the UI

The undo/redo buttons in [`ControlPanel.jsx`](https://github.com/drawdb-io/drawdb/blob/main/ControlPanel.jsx) wire directly to these functions:

```jsx
import { useUndoRedo } from '../../hooks/useUndoRedo';

export default function ControlPanel() {
  const { undoStack, redoStack } = useUndoRedo();

  return (
    <>
      <button 
        onClick={undo} 
        disabled={undoStack.length === 0}
      >
        Undo
      </button>
      <button 
        onClick={redo} 
        disabled={redoStack.length === 0}
      >
        Redo
      </button>
    </>
  );
}

```

## Special Cases: Bulk Operations and DBML Snapshots

**Bulk actions** handle multiple elements in one history entry. The `elements` array contains sub-actions with their own `undo` payloads, processed in sequence.

**DBML snapshots** use `swapDbmlSnapshot(a)`, which exchanges the current diagram state with a stored previous state—treated as atomic operations in the history.

## Summary

- **drawDB implements undo/redo through two React state arrays** (`undoStack`, `redoStack`) provided by `UndoRedoContext` in [`src/context/UndoRedoContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/UndoRedoContext.jsx)
- **Every edit creates an immutable action object** with standardized fields from `Action` and `ObjectType` enums defined in [`src/data/constants.js`](https://github.com/drawdb-io/drawdb/blob/main/src/data/constants.js)
- **The `undo` and `redo` functions in [`src/components/EditorHeader/ControlPanel.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/components/EditorHeader/ControlPanel.jsx)** interpret action types to apply inverse or forward operations using diagram-mutating APIs
- **Stack transfer is atomic**: popping from one stack always pushes to the opposite, preserving history continuity
- **Special handling exists for bulk edits, DBML snapshots, and relationship restoration** during table deletions

## Frequently Asked Questions

### What data structure does drawDB use for undo/redo history?

DrawDB uses **two simple JavaScript arrays** managed through React state. The `undoStack` stores completed actions; the `redoStack` stores actions that were undone. This stack-based approach keeps the implementation straightforward while supporting infinite traversal through edit history.

### Where is the undo/redo logic actually implemented?

The core logic lives in **[`src/components/EditorHeader/ControlPanel.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/components/EditorHeader/ControlPanel.jsx)**, specifically in the `undo()` and `redo()` functions. These functions consume the `UndoRedoContext` from [`src/context/UndoRedoContext.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/context/UndoRedoContext.jsx) and invoke diagram-mutating utilities like `deleteTable()`, `updateField()`, and `addRelationship()`.

### How does drawDB prevent creating new history entries when undoing?

All diagram-mutation functions accept a **`false` parameter** as their last argument. When `false`, these functions skip pushing to the undo stack. The `undo` and `redo` functions pass `false` to every mutation, ensuring that reversal operations don't generate duplicate history entries.

### Can drawDB undo complex operations like bulk table moves?

Yes. DrawDB's action object includes a **`bulk` boolean and `elements` array** for batch operations. When `bulk` is true, the undo/redo functions iterate through all sub-elements and apply the appropriate inverse or forward operation to each, treating the entire batch as single history entry.