Course navigationPhase 25
Volume 07 · Phase 25

Optimistic Updates

Add optimistic UI safely by defining snapshot, optimistic state, rollback, reconciliation, temporary identity, and concurrency behavior.

16lessons + labs
4klesson words
10source references
What you will ship

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.

Figure 25.1 — Where this phase lives in TaskFlow
user action
cancel/snapshot relevant queries
optimistic cache write
server mutation
rollback on error
invalidate/reconcile on settle
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Optimistic updates must define how to snapshot, apply, roll back, and reconcile state.

02

Use optimism where failure is uncommon and the user benefit justifies rollback complexity.

03

Handle concurrent mutations and stale responses so an older result cannot overwrite newer user intent.

04

Always reconcile optimistic client state with authoritative server state.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Choose one mutation where low failure probability and UX benefit justify optimism.

  2. 02

    Capture the previous cache state before changing it.

  3. 03

    Apply an optimistic update to the exact affected query data.

  4. 04

    Force a server failure and prove rollback restores the previous state.

  5. 05

    On success/settle, reconcile with authoritative server data rather than trusting the optimistic object forever.

  6. 06

    Simulate two rapid mutations and reason about race ordering and stale responses.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Optimism without rollback

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 2

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.

Failure drill 3

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.

Failure drill 4

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.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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

2 min
04

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

2 min
05

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

2 min
06

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

2 min
07

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

2 min
08

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

2 min
09

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

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

Guided Build Lab

Purpose Add optimistic UI safely by defining snapshot, optimistic state, rollback, reconciliation, temporary identity, and concurrency behavior.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 25 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Choose one mutation where low failure probability and UX benefit justify optimism.

Practice

Apply an optimistic update to the exact affected query data.

Debug

reproduce and diagnose — Optimism without rollback.

Independent

deliver Optimistic task update/create flow with rollback and authoritative server reconciliation.

Explain-back

Can implement snapshot/rollback/reconcile and Can decide when optimism is inappropriate.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 25's engineering goal?
02Which practice best matches Phase 25's engineering goal?
03Which practice best matches Phase 25's engineering goal?
04Which practice best matches Phase 25's engineering goal?
05You are working through the Phase 25 lab. Which next action is the most disciplined step?
06You are working through the Phase 25 lab. Which next action is the most disciplined step?
07You are working through the Phase 25 lab. Which next action is the most disciplined step?
08You are working through the Phase 25 lab. Which next action is the most disciplined step?
Source snapshot

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 |