Chrome Extension Undo Redo Patterns — Best Practices
6 min readUndo/Redo Patterns for Chrome Extensions
This guide covers implementing undo/redo functionality in Chrome extensions using proven patterns.
Command Pattern
The Command pattern is the foundation for undo/redo systems. Each action is wrapped as a command object:
interface Command {
execute(): Promise<void> | void;
undo(): Promise<void> | void;
}
class DeleteBookmarkCommand implements Command {
constructor(private bookmarkId: string, private bookmarkData: BookmarkItem) {}
execute(): void {
chrome.bookmarks.remove(this.bookmarkId);
}
undo(): void {
chrome.bookmarks.create({
title: this.bookmarkData.title,
url: this.bookmarkData.url,
parentId: this.bookmarkData.parentId
});
}
}
Stack Management
Maintain two stacks: one for undo and one for redo:
class UndoManager {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private maxDepth = 50;
async execute(command: Command): Promise<void> {
await command.execute();
this.undoStack.push(command);
this.redoStack = []; // Clear redo on new action
if (this.undoStack.length > this.maxDepth) {
this.undoStack.shift();
}
}
async undo(): Promise<void> {
const command = this.undoStack.pop();
if (command) {
await command.undo();
this.redoStack.push(command);
}
}
async redo(): Promise<void> {
const command = this.redoStack.pop();
if (command) {
await command.execute();
this.undoStack.push(command);
}
}
}
Snapshot vs Diff Approaches
Snapshot Approach
Save complete state before each change. Simpler to implement:
class SnapshotManager {
private history: Map<string, any>[] = [];
snapshot(key: string): void {
chrome.storage.local.get(key, (result) => {
this.history.push({ key, state: result[key] });
});
}
async undo(key: string): Promise<void> {
const snapshot = this.history.pop();
if (snapshot?.key === key) {
await chrome.storage.local.set({ [key]: snapshot.state });
}
}
}
Diff-Based Approach
Save only changes. More memory efficient for large states:
interface Diff {
key: string;
oldValue: any;
newValue: any;
}
class DiffManager {
private diffs: Diff[] = [];
recordDiff(key: string, oldValue: any, newValue: any): void {
this.diffs.push({ key, oldValue, newValue });
}
async undo(key: string): Promise<void> {
const diff = this.diffs.pop();
if (diff?.key === key) {
await chrome.storage.local.set({ [key]: diff.oldValue });
}
}
}
Storage Persistence
Serialize undo history to chrome.storage for persistence across sessions:
class PersistentUndoManager extends UndoManager {
async save(): Promise<void> {
const data = {
undoStack: this.undoStack,
redoStack: this.redoStack
};
await chrome.storage.local.set({ undoHistory: data });
}
async load(): Promise<void> {
const result = await chrome.storage.local.get('undoHistory');
if (result.undoHistory) {
// Restore stacks from serialized data
}
}
}
Keyboard Shortcuts
Register Ctrl+Z (undo) and Ctrl+Shift+Z or Ctrl+Y (redo) in manifest.json:
{
"commands": {
"undo": {
"suggested_key": "Ctrl+Z",
"description": "Undo last action"
},
"redo": {
"suggested_key": "Ctrl+Shift+Z",
"description": "Redo last action"
}
}
}
UI: Undo Toast Notification
Display a temporary toast with undo option:
function showUndoToast(message: string, onUndo: () => void): void {
const toast = document.createElement('div');
toast.className = 'undo-toast';
toast.innerHTML = `
<span>${message}</span>
<button class="undo-btn">Undo</button>
`;
toast.querySelector('.undo-btn').addEventListener('click', onUndo);
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 5000); // Auto-dismiss after 5s
}
Best Practices
- Limit stack size: Set maxDepth (50 recommended) to control memory
- Batch operations: Combine related small operations into single undoable actions
- Handle destructive actions carefully: Always store sufficient data to restore state
- Persist critical undo history: Use chrome.storage for important operations
- Provide clear feedback: Show undo toast for user actions
Related Patterns
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.