Documenteert het patroon dat in Stories 1-8 is opgeleverd, zodat een volgende workspace-store (sprint, of een nieuwe bounded context) hetzelfde recept volgt. - docs/patterns/workspace-store.md (nieuw): wanneer een workspace-store, de vijf state-slices, selectors-regels (G1/G2), race-safe ensure*Loaded met activeRequestId-guard (G4), SSE-hook + applyRealtimeEvent met unknown-event filter, hidden-tab + reconnect resync via useWorkspaceResync, restore-hint flow met await-chain en URL-prioriteit, optimistic mutations (applyOptimisticMutation/rollback/settle), API endpoint-vereisten (force-dynamic, cache: no-store), test-setup met MemoryStorage + originalActions snapshot + mockImplementation, gotchas G1-G8 als comment-template, en het 8-staps migratiepad. - docs/patterns/zustand-optimistic.md: bijgewerkt voor de nieuwe workspace-store API; verwijst voor het bredere patroon naar workspace-store.md. Voorbeelden voor pbi-order + entity-patch. - CLAUDE.md: patterns quickref aangevuld met workspace-store-rij. Verify: typecheck clean. Refs: PBI-74 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.4 KiB
Markdown
101 lines
3.4 KiB
Markdown
---
|
|
title: "Zustand optimistische update + rollback"
|
|
status: active
|
|
audience: [ai-agent, contributor]
|
|
language: nl
|
|
last_updated: 2026-05-10
|
|
when_to_read: "When adding client-side state mutations that need optimistic UI and rollback (DnD, status toggles)."
|
|
---
|
|
|
|
# Patroon: Zustand optimistische update + rollback
|
|
|
|
Sinds PBI-74 lopen optimistic mutations via `applyOptimisticMutation`/
|
|
`rollbackMutation`/`settleMutation` op de **workspace-store**. Het bredere
|
|
patroon (store-design, SSE-integratie, restore-hints, tests) staat in
|
|
[workspace-store.md](./workspace-store.md). Dit document beschrijft het
|
|
DnD/status-mutation flow specifiek.
|
|
|
|
## Patroon
|
|
|
|
1. Snapshot rollback-info via `applyOptimisticMutation` — krijgt `mutationId`.
|
|
2. Pas state direct aan via `setState`.
|
|
3. Server-actie aanroepen.
|
|
4. Op success: `settleMutation(mutationId)` (ruimt pending-record op).
|
|
5. Op error: `rollbackMutation(mutationId)` (herstelt vorige state + toast).
|
|
|
|
Cross-priority drag vereist twee mutaties (order + entity-patch) die samen
|
|
settlen of rollbacken.
|
|
|
|
## Voorbeeld — PBI reorder
|
|
|
|
```tsx
|
|
import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
|
|
|
|
function handleDragEnd(event: DragEndEvent) {
|
|
const { active, over } = event
|
|
if (!over || active.id === over.id) return
|
|
|
|
const store = useProductWorkspaceStore.getState()
|
|
const prevOrder = [...store.relations.pbiIds]
|
|
const oldIndex = prevOrder.indexOf(active.id as string)
|
|
const newIndex = prevOrder.indexOf(over.id as string)
|
|
if (oldIndex === -1 || newIndex === -1) return
|
|
const newOrder = arrayMove([...prevOrder], oldIndex, newIndex)
|
|
|
|
// 1. Snapshot rollback-info
|
|
const mutationId = store.applyOptimisticMutation({
|
|
kind: 'pbi-order',
|
|
prevPbiIds: prevOrder,
|
|
})
|
|
|
|
// 2. Optimistisch toepassen
|
|
useProductWorkspaceStore.setState((s) => {
|
|
s.relations.pbiIds = newOrder
|
|
})
|
|
|
|
// 3-5. Server bevestigt of niet
|
|
startTransition(async () => {
|
|
const result = await reorderPbisAction(productId, newOrder)
|
|
const st = useProductWorkspaceStore.getState()
|
|
if (result.success) {
|
|
st.settleMutation(mutationId)
|
|
} else {
|
|
st.rollbackMutation(mutationId)
|
|
toast.error('Volgorde opslaan mislukt')
|
|
}
|
|
})
|
|
}
|
|
```
|
|
|
|
## Voorbeeld — entity-patch (priority-wijziging)
|
|
|
|
```tsx
|
|
const prevPbi = store.entities.pbisById[id]
|
|
const patchMutationId = store.applyOptimisticMutation({
|
|
kind: 'entity-patch',
|
|
entity: 'pbi',
|
|
id,
|
|
prev: prevPbi,
|
|
})
|
|
useProductWorkspaceStore.setState((s) => {
|
|
const pbi = s.entities.pbisById[id]
|
|
if (pbi) pbi.priority = newPriority
|
|
})
|
|
// settle/rollback identiek aan order-flow
|
|
```
|
|
|
|
## Mutation-soorten
|
|
|
|
| `kind` | Rollback-data | Use-case |
|
|
|---|---|---|
|
|
| `pbi-order` | `prevPbiIds` | DnD reorder van PBI's |
|
|
| `story-order` | `pbiId` + `prevStoryIds` | DnD reorder van stories binnen een PBI |
|
|
| `task-order` | `storyId` + `prevTaskIds` | DnD reorder van tasks binnen een story |
|
|
| `entity-patch` | `entity` + `id` + `prev` (volledig vorig record of `undefined` voor delete-rollback) | Property-wijzigingen (priority, status), of optimistic delete/undelete |
|
|
|
|
## SSE-echo idempotent verwerken
|
|
|
|
Wanneer de server bevestigt en de NOTIFY-trigger het bijbehorende event
|
|
emitteert, mag `applyRealtimeEvent` **geen dubbele insert** veroorzaken en
|
|
**geen rollback triggeren**. INSERTs checken bestaan; UPDATEs mergen
|
|
into-existing. Zie `applyRealtimeEvent` in [`stores/product-workspace/store.ts`](../../stores/product-workspace/store.ts).
|