Optimistic updates must define how to snapshot, apply, roll back, and reconcile state.
Course navigationPhase 25⌄
Optimistic Updates
Add optimistic UI safely by defining snapshot, optimistic state, rollback, reconciliation, temporary identity, and concurrency behavior.
Phase outcome and project increment
Learning outcome
Add optimistic UI safely by defining snapshot, optimistic state, rollback, reconciliation, temporary identity, and concurrency behavior.
Concrete deliverable
Optimistic task update/create flow with rollback and authoritative server reconciliation.
Principles to keep while coding
Use optimism where failure is uncommon and the user benefit justifies rollback complexity.
Handle concurrent mutations and stale responses so an older result cannot overwrite newer user intent.
Always reconcile optimistic client state with authoritative server state.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Choose one mutation where low failure probability and UX benefit justify optimism.
- 02
Capture the previous cache state before changing it.
- 03
Apply an optimistic update to the exact affected query data.
- 04
Force a server failure and prove rollback restores the previous state.
- 05
On success/settle, reconcile with authoritative server data rather than trusting the optimistic object forever.
- 06
Simulate two rapid mutations and reason about race ordering and stale responses.
Failures you should know how to diagnose
Optimism without rollback
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Temporary IDs colliding with real IDs
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Older mutation response overwriting newer user intent
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Optimistically updating the wrong filtered list
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Optimistic UI Mental Model and When to Use It
Optimistic UI Mental Model and When to Use It Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture What optimistic means The user performs an action and TaskFlow immed
UI Optimism with Mutation Variables
UI Optimism with Mutation Variables Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Start with the simpler strategy Two optimistic strategies in TanStack Query UI
Cache Optimism onMutate Snapshot and Rollback
Cache Optimism: onMutate, Snapshot and Rollback Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture When cache optimism is justified If multiple components observe th
Query Cancellation and Stale Refetch Races
Query Cancellation and Stale Refetch Races Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture The race Why cancelQueries comes before the optimistic cache write Time
Server Reconciliation onSuccess onSettled and Invalidation
Server Reconciliation: onSuccess, onSettled and Invalidation Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Optimism ends when the server responds The local pred
Filtered and Paginated Caches
Filtered and Paginated Caches Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture The hard part is list membership One mutation can affect several cached views differ
Optimistic Create and Temporary Identity
Optimistic Create and Temporary Identity Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Creating is different from updating An update already has a stable server
Concurrent Mutations and Mutation Scopes
Concurrent Mutations and Mutation Scopes Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Mutations run in parallel by default Two rapid clicks can produce two ind
Rollback Error Semantics and Business Rejection
Rollback, Error Semantics and Business Rejection Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Failure is part of the design An optimistic UI is incomplete unti
Accessibility and Optimistic UX
Accessibility and Optimistic UX Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Instant does not mean invisible Visual speed should not remove meaningful feedback
Testing Debugging and Devtools
Testing, Debugging and Devtools Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture What to test Optimistic code should be tested as a state machine, not only as a su
Hands On Lab and Phase Exit
Hands-On Lab and Phase Exit Phase 25 - Optimistic Updates Volume 07 - Advanced Application Architecture Lab 1 - UI optimism Convert the Task status control to render mutation.variables while
Guided Build Lab
Purpose Add optimistic UI safely by defining snapshot, optimistic state, rollback, reconciliation, temporary identity, and concurrency behavior.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 25 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 25 without following a transcript.
Exercises that increase independence
Choose one mutation where low failure probability and UX benefit justify optimism.
Apply an optimistic update to the exact affected query data.
reproduce and diagnose — Optimism without rollback.
deliver Optimistic task update/create flow with rollback and authoritative server reconciliation.
Can implement snapshot/rollback/reconcile and Can decide when optimism is inappropriate.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_Optimistic_Updates_Snapshot/OPTIMISTIC_DECISION_TABLE.md
# Optimistic Update Decision Table | Operation | Course decision | Why | |---|---|---| | Task status toggle | Optimistic | Small, reversible, predictable result | | Task title edit | Usually optimistic | Predictable, but preserve server validation errors | | Create Task | UI optimism first | Avoid inventing fake permanent IDs | | Delete Task | Usually pessimistic initially | Destructive; accidental false success is costly | | Membership role change | Pessimistic | Security-sensitive authorization operation | | Payroll finalization | Pessimistic | High-impact irreversible business workflow | | Purchase-order approval | Usually pessimistic | Approval rules may fail and user should not see false completion |
reference/TaskFlow_Optimistic_Updates_Snapshot/README.md
# TaskFlow Phase 25 - Optimistic Updates Snapshot This snapshot extends the Phase 22 TanStack Query architecture. It intentionally demonstrates two strategies: 1. UI optimism through mutation variables (preferred when one view needs the prediction). 2. Cache optimism through onMutate + rollback (used when several observers share the same Task detail cache). The server remains authoritative. Task list caches are invalidated after settlement rather than attempting to reproduce every filter/pagination rule in client code.
reference/TaskFlow_Optimistic_Updates_Snapshot/SOURCES.md
# Sources - https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates - https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation - https://tanstack.com/query/latest/docs/framework/react/guides/invalidations-from-mutations - https://tanstack.com/query/latest/docs/framework/react/guides/mutations - https://tanstack.com/query/latest/docs/framework/react/reference/functions/useMutationState - https://tanstack.com/query/latest/docs/reference/QueryClient
reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/components/tasks/pending-task-rows.tsx
"use client";
import { usePendingTaskCreates } from "@/hooks/use-create-task-pending";
export function PendingTaskRows() {
const pending = usePendingTaskCreates();
return pending.map(item => (
<tr key={item.submittedAt} aria-busy="true">
<td>{item.input.title}</td>
<td>Pending creation</td>
<td>{item.input.priority ?? "MEDIUM"}</td>
</tr>
));
}reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/components/tasks/task-status-button.tsx
"use client";
import { useTaskStatusOptimistic } from "@/hooks/use-task-status-optimistic";
import type { TaskStatus } from "@/lib/api/task-types";
type Props = {
taskId: string;
status: TaskStatus;
};
export function TaskStatusButton({ taskId, status }: Props) {
const mutation = useTaskStatusOptimistic(taskId);
const visibleStatus = mutation.isPending
? mutation.variables
: status;
const nextStatus: TaskStatus =
visibleStatus === "DONE" ? "IN_PROGRESS" : "DONE";
reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/components/ui/task-saving-indicator.tsx
"use client";
import { useIsMutating } from "@tanstack/react-query";
export function TaskSavingIndicator() {
const count = useIsMutating({ mutationKey: ["tasks", "mutation"] });
if (count === 0) return null;
return (
<span role="status" aria-live="polite">
Saving {count === 1 ? "change" : `${count} changes`}...
</span>
);
}reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/hooks/use-create-task-pending.ts
"use client";
import { useMutation, useMutationState } from "@tanstack/react-query";
import { createTask } from "@/lib/api/task-mutations";
import { taskKeys } from "@/lib/query/task-keys";
import { taskMutationKeys } from "@/lib/query/task-mutation-keys";
import type { CreateTaskInput } from "@repo/validators/tasks";
export function useCreateTask() {
return useMutation({
mutationKey: taskMutationKeys.create(),
mutationFn: createTask,
onSettled: async (_data, _error, _variables, _result, context) => {
await context.client.invalidateQueries({
queryKey: taskKeys.lists(),
});
},
});
}
reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/hooks/use-task-status-optimistic.ts
"use client";
import { useMutation } from "@tanstack/react-query";
import { taskKeys } from "@/lib/query/task-keys";
import { taskMutationKeys } from "@/lib/query/task-mutation-keys";
import { updateTask } from "@/lib/api/task-mutations";
import type { Task, TaskStatus } from "@/lib/api/task-types";
export function useTaskStatusOptimistic(taskId: string) {
return useMutation({
mutationKey: taskMutationKeys.updateStatus(taskId),
scope: { id: `task-status:${taskId}` },
mutationFn: (status: TaskStatus) =>
updateTask(taskId, { status }),
onMutate: async (status, context) => {
const key = taskKeys.detail(taskId);
await context.client.cancelQueries({ queryKey: key });reference/TaskFlow_Optimistic_Updates_Snapshot/apps/web/src/lib/query/task-mutation-keys.ts
export const taskMutationKeys = {
all: ["tasks", "mutation"] as const,
create: () => [...taskMutationKeys.all, "create"] as const,
updateStatus: (taskId: string) =>
[...taskMutationKeys.all, "status", taskId] as const,
};reference/TaskFlow_Optimistic_Updates_Snapshot/tests/optimistic-update-test-matrix.md
# Phase 25 Optimistic Update Test Matrix | Scenario | Expected result | |---|---| | Status mutation starts | Predicted status appears immediately | | Detail query is refetching | cancelQueries prevents stale response from overwriting prediction | | Server accepts status | Detail cache uses server Task; lists invalidate | | Server returns 403 | Previous Task detail is restored; permission error shown | | Server returns 409 | Previous Task detail is restored; conflict explained | | Network fails | Previous Task detail is restored; retry remains possible | | Two changes same Task | Same scope serializes requests in intent order | | Two changes different Tasks | Requests can run independently | | Create Task pending | Temporary UI row uses mutation variables + submittedAt | | Create succeeds | Pending row disappears after list refetch returns server Task | | Status-filtered list | List is reconciled by invalidation/refetch rather than fabricated client metadata | | Logout while mutations exist | Existing Phase 22 logout clears QueryClient cache |