Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
IT Nachrichten22. September(22.09.2026 um 00:05 Uhr)
IT NachrichtenLizenzprobleme: AnyDesk und TeamViewer(22.09.2026 um 00:30 Uhr)
Apple iOS & macOSApple's iOS 27.2 beta 2 reveals new anti-snatching protections(22.09.2026 um 00:27 Uhr)
AI & KI NachrichtenUC Irvine to Study AI for Writing Instruction(21.09.2026 um 23:31 Uhr)
AI & KI NachrichtenBurnham to call for global effort to control threats posed by AI(21.09.2026 um 23:30 Uhr)
IT Nachrichten22. September(22.09.2026 um 00:05 Uhr)
IT NachrichtenLizenzprobleme: AnyDesk und TeamViewer(22.09.2026 um 00:30 Uhr)
Apple iOS & macOSApple's iOS 27.2 beta 2 reveals new anti-snatching protections(22.09.2026 um 00:27 Uhr)
AI & KI NachrichtenUC Irvine to Study AI for Writing Instruction(21.09.2026 um 23:31 Uhr)
AI & KI NachrichtenBurnham to call for global effort to control threats posed by AI(21.09.2026 um 23:30 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Time Travel for Your State: Undo/Redo - with Zustand and React Query (Part 2)

In Part 1, we discussed the snapshot pattern with past/present/future arrays for undo and redo. If you haven't checked it out, you can do so here. Some background In a personal project I've been working on, I spent the past…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

In Part 1, we discussed the snapshot pattern with past/present/future arrays for undo and redo. If you haven't checked it out, you can do so here.






Some background



In a personal project I've been working on, I spent the past week trying to find a clean, generic way to implement undo/redo without ending up with a bunch of hardcoded states. I wanted something that could manage itself instead of turning into a mess over time.



The first approach with past, present, and future arrays? Elegant. Clean. But as I tried integrating it into my actual project, I started running into something that felt off.



I'm using Zustand for client-side state and Tanstack Query for server state. React Query helps me make fewer network calls by caching data, so persisting full snapshots for every undo step started feeling heavy and redundant.



The last approach still works great in many setups (especially Redux-like flows), but in my current stack it meant extra bookkeeping in Zustand just to track history that was adjacent to data React Query was already managing.






Store Functions, Not Data



Instead of storing what the state was, what if we stored how to restore it?



I found this approach in Rocicorp's undo library, and it made a lot of sense. Instead of snapshots, you store instructions—a pair of undo and redo functions:




{
entries: [
{ undo: fn1, redo: fn2 },
{ undo: fn3, redo: fn4 },
{ undo: fn5, redo: fn6 }
],
index: 1 // Currently at entry[1]
}






Each entry is a pair of functions: one that undoes an action, one that redoes it. The index tells you where you are in history.




  • Want to undo? Run entries[index].undo() and move index back

  • Want to redo? Move index forward and run entries[index].redo()






Building the UndoManager



Here's the core class:




type Entry = {
groupId?: number;
undo: () => Promise<void>;
redo: () => Promise<void>;
};

class UndoManager {
private state = {
entries: [] as Entry[],
index: -1, // -1 means no history yet
isGrouping: false,
lastGroupId: 0,
};

get canUndo(): boolean {
return this.state.index >= 0;
}

get canRedo(): boolean {
return this.state.index < this.state.entries.length - 1;
}

async add(options: { undo: () => Promise<void>; redo: () => Promise<void> }) {
// Clear any "future" entries
this.state.entries.splice(this.state.index + 1);

// Add new entry
this.state.entries.push({
undo: options.undo,
redo: options.redo,
groupId: this.state.isGrouping ? this.state.lastGroupId : undefined,
});

this.state.index += 1;
}

async undo() {
if (!this.canUndo) return;

const entry = this.state.entries[this.state.index];
this.state.index -= 1;
await entry.undo();
}

async redo() {
if (!this.canRedo) return;

const entry = this.state.entries[this.state.index + 1];
this.state.index += 1;
await entry.redo();
}
}






The key insight: index points to the "current" entry. When index is -1, you're at the beginning with no history. When you undo, you execute the current entry's undo function and move the pointer back.






Integrating with React Query



This is where it gets interesting. Here's how I wrapped mutations to automatically add undo/redo support:




const runWithSnapshotUndo = async (execute) => {
// Capture state before
const beforeSnapshot = await getSnapshot();

// Run the actual mutation
const result = await execute();

// Capture state after
const afterSnapshot = await getSnapshot();

// Compare - if nothing changed, don't add to history
if (areSnapshotsEqual(beforeSnapshot, afterSnapshot)) {
return result;
}

// Add undo/redo functions
await undoManager.add({
undo: () => applySnapshot(beforeSnapshot),
redo: () => applySnapshot(afterSnapshot),
});

return result;
};






Now any mutation can be wrapped:




const addTodoWithUndo = (payload) =>
runWithSnapshotUndo(() =>
addTodoMutation.mutateAsync(payload)
);









Why Compare Snapshots?



The areSnapshotsEqual check might seem unnecessary, but it prevents a common bug: adding no-op entries to history.



Imagine you click "save" but nothing changed. Without the comparison, you'd add an entry to undo... nothing. Hit Cmd+Z and the user is confused because nothing happened.




const areSnapshotsEqual = (a: Todo[], b: Todo[]): boolean => {
if (a.length !== b.length) return false;

for (let i = 0; i < a.length; i++) {
const left = a[i];
const right = b[i];

if (
left.id !== right.id ||
left.text !== right.text ||
left.completed !== right.completed
// ... check all relevant fields
) {
return false;
}
}

return true;
};









Bonus 1: Grouping Actions



What if one user action triggers multiple mutations?



Example: dragging a task to a new position in a board.




  • Update task A's position

  • Shift task B down

  • Shift task C down



Technically that's three writes, but the user thinks of it as one action. They expect one Cmd+Z to undo the entire reorder.



That's what groupId is for:




undoManager.startGroup();
await updateOperation1();
await updateOperation2();
await updateOperation3();
undoManager.endGroup();

// All three share the same groupId
// One undo() will revert all three






The undo implementation handles this recursively:




async undo() {
if (!this.canUndo) return;

const entry = this.state.entries[this.state.index];
this.state.index -= 1;
await entry.undo();

const nextEntry = this.state.entries[this.state.index];

// If next entry has the same groupId, keep undoing
if (
entry.groupId !== undefined &&
nextEntry &&
nextEntry.groupId === entry.groupId
) {
await this.undo();
}
}









Bonus 2: Keyboard Shortcuts



Since this is a standalone manager, I built keyboard shortcut support directly in:




attachKeyboardShortcuts(getContainer: () => HTMLElement | null) {
const onKey = (e: KeyboardEvent) => {
const el = getContainer();
if (!el || !el.contains(e.target)) return;

// Don't intercept if user is typing in input/textarea
const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;

if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
void this.undo();
} else if (
(e.metaKey || e.ctrlKey) &&
(e.key === 'y' || (e.key === 'z' && e.shiftKey))
) {
e.preventDefault();
void this.redo();
}
};

window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}






Call it once when your app mounts, and Cmd+Z / Cmd+Shift+Z just work.






What I Like About This



The command pattern feels more composable. You can:




  • Add undo to specific mutations without touching others

  • Group related actions easily

  • Handle async operations naturally

  • Keep memory usage low



It's not tied to any state management library. It's just: "here's how to undo this, here's how to redo it."



Your code stays decoupled, and undo/redo becomes something you layer on top, not something you architect your entire app around.




Memory still matters: even when you're storing functions, you can still accumulate thousands of entries. Set a max size (I use 1,000) and trim older history.







Try It Out



If you want to skip the implementation, check out Rocicorp's undo library that inspired this approach. But building it yourself gives you full control over grouping, async behavior, and integration with your specific state management.



If you want the full code, let me know and I'll put together a sample project.






In Part 1, we explored the snapshot pattern with past/present/future. In this part, we saw how the command pattern gives you more control and better performance for complex apps.



Both approaches work. The snapshot way is great for pure state transformations. The command way is great when you need flexibility, async operations, and tighter control over what gets grouped. The right choice can change based on your state management and your use case.



Pick the one that fits your use case. Or, if you're like me, try both and see which one clicks.



Thanks for reading. If you've implemented undo/redo in your own projects, I'd love to hear how you approached it. And if you know a better way, definitely share it.



Until next time 👋

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Time Travel for Your State: Undo/Redo - with Zustand and React Query (Part 2)

Thematisch verwandte Begriffe: Time, Travel, Your, State · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-49449 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick