Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Optimistic Updates for the Task Toggle

Optimistic Updates for the Task Toggle

~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

PREVIEWED in chapter 88, mentioned as a CONCEPT in chapter 78 – NOW useToggleTaskDone gets EXTENDED with immediate visual feedback, BEFORE the server response arrives.

The problem without an optimistic update

WITHOUT optimization, the checkbox waits for the COMPLETE server response (network latency PLUS server processing) BEFORE the displayed state changes – on a SLOW connection this feels SLUGGISH, EVEN THOUGH the action ALMOST ALWAYS succeeds.

Implementing onMutate

src/hooks/useToggleTaskDone.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import type { Task, TaskCollection } from '../types/task';

interface ToggleTaskInput {
  taskId: number;
  done: boolean;
  projectId: number;
}

export function useToggleTaskDone() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ taskId, done }: ToggleTaskInput) => {
      const response = await apiClient.patch<Task>(
        `/tasks/${taskId}`,
        { done },
        { headers: { 'Content-Type': 'application/merge-patch+json' } },
      );

      return response.data;
    },
    onMutate: async ({ taskId, done, projectId }) => {
      const queryKey = ['projects', projectId, 'tasks'];

      await queryClient.cancelQueries({ queryKey });

      const previousTasks = queryClient.getQueryData<TaskCollection>(queryKey);

      queryClient.setQueryData<TaskCollection>(queryKey, (old) => {
        if (!old) {
          return old;
        }

        return {
          ...old,
          'hydra:member': old['hydra:member'].map((task) =>
            task.id === taskId ? { ...task, done } : task,
          ),
        };
      });

      return { previousTasks, queryKey };
    },
    onError: (_err, _variables, context) => {
      if (context) {
        queryClient.setQueryData(context.queryKey, context.previousTasks);
      }
    },
    onSettled: (_data, _error, variables) => {
      queryClient.invalidateQueries({
        queryKey: ['projects', variables.projectId, 'tasks'],
      });
    },
  });
}

FOUR phases: onMutate changes the cache IMMEDIATELY and SAVES the OLD state, onError RESTORES it on failure, onSettled (runs ALWAYS, SUCCESS or FAILURE) synchronizes with the REAL server state AT the end.

Achtung: cancelQueries MATTERS: WITHOUT this call, a SIMULTANEOUSLY running background refetch (chapter 73: refetch on window focus) could OVERWRITE the OPTIMISTIC update with STALE server data, EVEN BEFORE the mutation has finished AT ALL.

Why context carries the old state

onMutate's RETURN value gets AUTOMATICALLY passed as the THIRD argument to onError AND onSettled – TanStack Query calls this "context", a mechanism designed EXACTLY FOR this purpose (SHARING rollback data between the callbacks).

Testing the behavior

Enable network throttling in the browser DevTools ("Slow 3G") and click the checkbox – the CHECKED state changes IMMEDIATELY, WHILE the actual request is STILL running in the background, EXACTLY the DESIRED effect.

Tipp: Optimistic updates pay off ESPECIALLY for interactions with a HIGH success rate and HIGH frequency (checkboxes, likes, favorites) – for RARE, ERROR-PRONE actions (like deleting, chapter 84), the SIMPLER "wait and then update" pattern usually remains the BETTER choice.