Skip to main content

Command Pattern

Encapsulate a request as an object (or function pair), allowing you to parameterize, queue, log, and undo operations.


The Problem​

You're building a text editor. Users expect Ctrl+Z to undo. But your operations are scattered:

function insertText(doc: Doc, text: string, pos: number) {
doc.content = doc.content.slice(0, pos) + text + doc.content.slice(pos);
// How do we undo this?
}

function deleteText(doc: Doc, start: number, end: number) {
doc.content = doc.content.slice(0, start) + doc.content.slice(end);
// What was deleted? Can't undo without knowing.
}

Operations don't know how to reverse themselves. Undo is impossible.


The Solution​

Each operation is a pair: execute and undo. Stack them for history:

import { undoable, createCommandStack } from "@pithos/core/eidos/command/command";

const doc = { content: "Hello" };
const stack = createCommandStack();

// Insert "World" at position 5
const pos = 5;
const text = " World";

const insertCmd = undoable(
() => { doc.content = doc.content.slice(0, pos) + text + doc.content.slice(pos); },
() => { doc.content = doc.content.slice(0, pos) + doc.content.slice(pos + text.length); }
);

stack.execute(insertCmd); // doc.content = "Hello World"
stack.undo(); // doc.content = "Hello"
stack.redo(); // doc.content = "Hello World"

Every operation knows how to reverse itself. Full undo/redo for free.

Reactive variant​

The imperative version above works great with Vue, Angular, and Svelte where mutation triggers reactivity. For React and other immutable-state systems, use the reactive variant where commands are pure (state) => state transforms:

import { undoableState, createReactiveCommandStack } from "@pithos/core/eidos/command/command";

interface Doc { content: string }

const stack = createReactiveCommandStack<Doc>({
initial: { content: "Hello" },
onChange: setDoc, // React setState, Vue ref, Angular signal...
});

const pos = 5;
const text = " World";

stack.execute(undoableState(
(doc) => ({ ...doc, content: doc.content.slice(0, pos) + text + doc.content.slice(pos) }),
(doc) => ({ ...doc, content: doc.content.slice(0, pos) + doc.content.slice(pos + text.length) }),
));
// onChange fires with { content: "Hello World" }

stack.undo(); // onChange fires with { content: "Hello" }
stack.redo(); // onChange fires with { content: "Hello World" }

Same pattern, no mutation. The stack manages state and notifies your framework.


Command vs Memento​

Both patterns enable undo/redo, but they work differently:

  • Command tracks individual operations and their reversal. Use when operations are known and reversible.
  • Memento captures entire state snapshots. Use when state is complex or operations aren't easily reversible.

Live Demo​

Code
/**
* Kanban command stack using Pithos's reactive command pattern.
*
* undoableState() creates pure (State) => State transforms.
* createReactiveCommandStack manages state + undo/redo + onChange notifications.
*/

import { undoableState, createReactiveCommandStack } from "@pithos/core/eidos/command/command";
import { COLUMNS, INITIAL_BOARD } from "@/data/kanban";
import type { BoardState, ColumnId, HistoryEntry } from "./types";

function moveTask(board: BoardState, taskId: string, from: ColumnId, to: ColumnId): BoardState {
const task = board[from].find((t) => t.id === taskId);
if (!task) return board;
return { ...board, [from]: board[from].filter((t) => t.id !== taskId), [to]: [...board[to], task] };
}

export function createKanbanStack(onChange: (board: BoardState) => void) {
const stack = createReactiveCommandStack<BoardState>({ initial: INITIAL_BOARD, onChange });
const log: HistoryEntry[] = [];
let cursor = -1;
let entryId = 0;

return {
move(taskId: string, from: ColumnId, to: ColumnId) {
const task = stack.state[from].find((t) => t.id === taskId);
if (!task) return;
stack.execute(undoableState(
(b) => moveTask(b, taskId, from, to),
(b) => moveTask(b, taskId, to, from),
));
log.length = cursor + 1;
entryId++;
log.push({ id: entryId, description: `Move "${task.title}" from ${COLUMNS[from].label} to ${COLUMNS[to].label}` });
cursor = log.length - 1;
},
undo(): boolean { const ok = stack.undo(); if (ok) cursor--; return ok; },
redo(): boolean { const ok = stack.redo(); if (ok) cursor++; return ok; },
clear() { stack.clear(); log.length = 0; cursor = -1; entryId = 0; },
get state(): BoardState { return stack.state; },
get canUndo(): boolean { return stack.canUndo; },
get canRedo(): boolean { return stack.canRedo; },
get history(): readonly HistoryEntry[] { return log; },
get cursor(): number { return cursor; },
};
}
Result

Real-World Analogy​

A macro recorder in a spreadsheet. You record a sequence of actions (commands), replay them on different data (queue), undo mistakes (undo), and save the macro for later (serialize). Each action is a discrete, reversible unit.


When to Use It​

  • Implement undo/redo functionality
  • Queue operations for later execution
  • Log all operations for audit trails
  • Support transactional behavior (rollback on failure)

When NOT to Use It​

If your operations aren't reversible or you just need to snapshot state, consider Memento instead.


API​