Engineering Journal
Table Formatter
Table Formatter

When Undo Lies: Fixing a Shared History Stack in a Multi-Table Editor

2026-05-30

TLDR

Sharing a single undo history array across multiple spreadsheet tables causes cross-context state pollution. Editing table A consumes available history slots for table B, resulting in inaccurate undo depths. Replacing global arrays with a Map keyed by ${sheetId}::${tableId} gives every table an independent 50-state stack.
History ArchitectureStorage ScopeTable Switch BehaviorUndo Isolation
Global Shared Array#tableContainer innerHTMLOverwrites unrelated tablesFails (Edits contaminate other tables)
Per-Slot Context MapTarget table.outerHTMLIn-place replaceWith swap100% Independent Per Table

Global history arrays pollute undo buffers and trigger cross-context data corruption

Our initial history manager stored full container snapshots in a global 50-item array. In multi-table documents, editing table A ten times consumed 10 slots from the global history pool.

When users switched to table B and pressed undo, the manager restored a snapshot captured during table A's editing sequence, corrupting table B's state.


Keying undo stacks in a Map and swapping targeted DOM elements isolates editing context

To isolate history stacks per table, we replaced the shared array with a Map keyed by ${sheetId}::${tableId}:

   GLOBAL SHARED HISTORY (Vulnerable to cross-pollution):
   [Edits on Table A] ──┐
   [Edits on Table B] ──┼─> [ Single Stack: [A1, A2, B1, A3, B2] ]
   [Edits on Table C] ──┘
                             (Undo on Table B restores A3! State corrupted!)

KEYED HISTORY MAP (Isolated contexts): Map: ┌─────────────────┬──────────────────────────────────────┐ │ Context Key │ History Stack │ ├─────────────────┼──────────────────────────────────────┤ │ Sheet1::TableA │ [ A1, A2, A3 ] │ │ Sheet1::TableB │ [ B1, B2 ] │ │ Sheet2::TableC │ [ C1 ] │ └─────────────────┴──────────────────────────────────────┘ (Undo on Table B restores B1. Safe and isolated!)

Here is the implementation of our per-slot stack isolation:

// Per-slot history manager storing targeted table HTML snapshots
const slotHistoryMap = new Map();
let isRestoringState = false;

function getContextKey() { const sheetId = window.activeSheetId || 'default'; const tableId = window.currentTable ? window.currentTable.getAttribute('data-tifany-id') : 'default'; return ${sheetId}::${tableId}; }

export function saveSlotState() { if (isRestoringState || !window.currentTable) return; const key = getContextKey(); if (!slotHistoryMap.has(key)) { slotHistoryMap.set(key, { history: [], index: -1 }); }

const slot = slotHistoryMap.get(key); const tableHtml = window.currentTable.outerHTML;

if (slot.index >= 0 && slot.history[slot.index] === tableHtml) return;

slot.history = slot.history.slice(0, slot.index + 1); slot.history.push(tableHtml); if (slot.history.length > 50) slot.history.shift(); else slot.index++; }

During restoration, the editor replaces only the target table element using replaceWith(), keeping adjacent tables untouched.

Rule of thumb: Key history stacks to active context IDs (sheetId::tableId) and use in-place DOM replacements to isolate undo actions to a single component.
Read this post in the full Engineering Journal →