The Command Pattern: Because Undo Shouldn't Be Rocket Science 🚀
The Undo Hell Nightmare
Picture this: You're building a rich text editor. Users type, format, delete, paste images—and then inevitably smash Ctrl+Z expecting everything to magically reverse. Without the Command Pattern, you're staring into the abyss of "Undo Hell": a massive switch statement trying to track every possible action, or worse, attempting to reverse-engineer state changes like some kind of code archaeologist.
Been there? Yeah, we all have.
The Command Pattern saves you from this nightmare. Instead of building an undo system, you build a time machine. Wrap every action in a command object, stack them up, and undo becomes as simple as popping items off a stack. Don't pray for perfect state tracking—queue commands and let them handle their own cleanup.
The Universal Remote Reality Check
Let's start with something relatable. You've built the ultimate universal remote with three magic buttons—it can control anything! Naturally, you test it on your TV, your cat, and your spouse:
TV Remote – Works like a dream. Press TurnOn
→ TV lights up. Press TurnOff
→ darkness. Press ChangeChannel
→ new show appears. Finally, a receiver that actually receives commands! 📺
Cat Remote – Pure chaos. Press CuddleCommand
→ cat teleports to the opposite corner. Press StopBeggingForFood
→ cat inhales the entire bowl, then screams like she's been starving for decades. Press BehaveCommand
→ cat locks eyes with you and slowly pushes your coffee off the table. Classic. 😹
Spouse Remote – Press DoDishesCommand
... radio silence. Press ListenToMeCommand
... still buffering. Press FixFaucetCommand
... you know how this ends. 😂
Here's the beautiful part: same remote structure, completely different results. Each receiver interprets commands in their own way. The remote doesn't care if it's talking to a logical TV, a chaotic cat, or a mysteriously unresponsive spouse—it just fires the command and moves on.
That's Command Pattern in a nutshell: decouple the invoker from the receiver, wrap requests in neat packages, and let each receiver handle them however they see fit.
The Friday Afternoon Panic
Ever had your PM casually drop "Oh, can we add undo functionality?" on a Friday at 4 PM? If your code isn't already using Command Pattern, you're looking at a weekend of architectural surgery on a moving train.
Here's the thing: adding undo to existing code is like trying to install airbags in a car that's already driving. With Command Pattern, undo is baked in from day one.
Command Pattern: The Code Reality
Let's build this step by step, starting with the most cooperative receiver known to humankind: a light bulb. Unlike cats (or spouses), lights have exactly two states and always respond to commands.
// Command Interface - the contract for all commands
interface ICommand {
execute(): void;
unexecute(): void; // The magic sauce for undo
}
// Receiver - the thing that actually does work
class Light {
private isOn = false;
turnOn() {
this.isOn = true;
console.log("Light is ON");
}
turnOff() {
this.isOn = false;
console.log("Light is OFF");
}
}
// Commands - actions wrapped in objects
class LightOnCommand implements ICommand {
constructor(private light: Light) {}
execute() {
this.light.turnOn();
}
unexecute() {
this.light.turnOff(); // Knows how to undo itself
}
}
class LightOffCommand implements ICommand {
constructor(private light: Light) {}
execute() {
this.light.turnOff();
}
unexecute() {
this.light.turnOn(); // Every command is its own time machine
}
}
// Invoker - your remote control
class RemoteControl {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
constructor(private on: ICommand, private off: ICommand) {}
pressOn() {
this.on.execute();
this.undoStack.push(this.on);
this.redoStack = []; // Clear redo when new action happens
}
pressOff() {
this.off.execute();
this.undoStack.push(this.off);
this.redoStack = [];
}
pressUndo() {
if (this.undoStack.length > 0) {
const command = this.undoStack.pop()!;
command.unexecute(); // Command knows how to undo itself
this.redoStack.push(command);
}
}
pressRedo() {
if (this.redoStack.length > 0) {
const command = this.redoStack.pop()!;
command.execute();
this.undoStack.push(command);
}
}
}
// Usage - simple and clean
const light = new Light();
const onCommand = new LightOnCommand(light);
const offCommand = new LightOffCommand(light);
const remote = new RemoteControl(onCommand, offCommand);
remote.pressOn(); // Light is ON
remote.pressOff(); // Light is OFF
remote.pressUndo(); // Light is ON (undid the off command)
remote.pressRedo(); // Light is OFF (redid the off command)
Notice how the remote doesn't know anything about lights—it just executes commands. That's the magic of decoupling.
Frontend Reality: Where You've Seen This Before
Command Pattern isn't some academic exercise—you interact with it daily:
- Google Docs undo - Every keystroke is a command
- Figma/Canva operations - Drawing, moving, styling—all commands
- VS Code - Every edit, format, refactor is wrapped as a command
- Photo editors - Crop, filter, adjust—each step is undoable because it's a command
- Any app with Ctrl+Z - If it has undo, it probably uses Command Pattern
Anywhere you see "Edit → Undo" in a menu, someone implemented Command Pattern (or should have).
The Reusable Button Problem
Here's where Command Pattern becomes your secret weapon. Imagine a single Undo button you can drop anywhere in your React app—toaster notifications, modals, WYSIWYG editors. Same button, completely different behaviors.
The button doesn't know what it's undoing. Each context provides its own command:
// Command interface
interface UndoCommand {
execute(): void;
}
// Universal Undo Button - works anywhere
const UndoButton = ({ command }: { command: UndoCommand }) => (
<button onClick={() => command.execute()}>
↶ Undo
</button>
);
// Different commands for different contexts
const toasterUndo: UndoCommand = {
execute: () => showDeletedItemAgain(),
};
const modalUndo: UndoCommand = {
execute: () => reopenModal(),
};
const editorUndo: UndoCommand = {
execute: () => undoLastTextOperation(),
};
// Same button, different superpowers
function App() {
return (
<div>
<ToastNotification>
<UndoButton command={toasterUndo} />
</ToastNotification>
<Modal>
<UndoButton command={modalUndo} />
</Modal>
<TextEditor>
<UndoButton command={editorUndo} />
</TextEditor>
</div>
);
}
One button to rule them all. The Command Pattern makes your components truly reusable.
Memory Management: The Harsh Reality
Here's what the tutorials don't tell you: Command Pattern can eat your RAM for breakfast. Every undoable action lives in memory until you clear it. I learned this the hard way when users crashed our app by typing a novel.
Production-ready strategies:
- Cap your undo stack (50-100 commands max)
- Batch tiny operations (merge keystrokes into word-level commands)
- Compress repetitive actions (10 cursor movements → 1 "move right 10" command)
- Clear on major operations (file save, page navigation)
class SmartRemoteControl {
private undoStack: ICommand[] = [];
private readonly MAX_UNDO_HISTORY = 50;
execute(command: ICommand) {
command.execute();
this.undoStack.push(command);
// Prevent memory leaks
if (this.undoStack.length > this.MAX_UNDO_HISTORY) {
this.undoStack.shift(); // Goodbye, oldest command
}
}
}
Advanced Patterns: Level Up Your Game
Macro Commands - Group operations into super-commands:
// "Format Paragraph" = Select All + Bold + Center Align
const formatParagraph = new MacroCommand([
new SelectAllCommand(),
new BoldCommand(),
new AlignCenterCommand()
]);
// One undo reverses all three operations
Command Chaining - Build pipelines that auto-rollback on failure:
// Form submission pipeline
new CommandPipeline()
.add(new ValidateFormCommand())
.add(new SaveDraftCommand())
.add(new SubmitToAPICommand())
.add(new ShowSuccessCommand())
.executeWithRollback(); // If any step fails, previous steps auto-undo
When NOT to Use Command Pattern
Don't be that developer who turns everything into a command. Skip it for:
-
Simple toggles (
isDark ? 'dark' : 'light'
) - One-off actions that will never need undo
- Performance-critical paths (command dispatch has overhead)
- Basic CRUD where you're just calling a method
If you're creating commands just to call a single method with no undo needs, you're overthinking it. Not every button press needs to be a command.
The Bottom Line
Command Pattern turns chaos into order:
- Invokers (buttons, remotes) don't need to know what they control
- Commands wrap requests and handle their own undo logic
- Receivers do the actual work without knowing who called them
- You get undo/redo for free, plus amazing flexibility
It's the programming equivalent of having a universal remote that actually works. Your future self will thank you when requirements change next sprint (and they always do).
What's your Command Pattern war story? Drop it in the comments—we've all been there.
Top comments (0)