iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Optimistic UI

Optimistic updates in GraphQL clients: predict the mutation result locally so UI feels instant; reconcile when the server responds.

GraphQL — optimistic updates

EXAMPLE
// ===== Why optimistic =====
// Mutations have network latency. Without optimistic updates, the UI flickers
// while waiting. With them, the UI changes immediately; if the server fails, we roll back.

// ===== Apollo Client =====
import { useMutation, gql } from '@apollo/client';

const ADD_TODO = gql\`
  mutation Add($text: String!) {
    addTodo(text: $text) {
      id text done
    }
  }
\`;

const [addTodo] = useMutation(ADD_TODO, {
  optimisticResponse: (vars) => ({
    addTodo: {
      __typename: 'Todo',
      id: 'temp-' + Date.now(),
      text: vars.text,
      done: false,
    },
  }),
  update: (cache, { data }) => {
    cache.modify({
      fields: {
        todos(existing = []) {
          const newRef = cache.writeFragment({
            data: data.addTodo,
            fragment: gql\`fragment Todo on Todo { id text done }\`,
          });
          return [...existing, newRef];
        },
      },
    });
  },
});

// ===== urql (graphCache) =====
// npm install @urql/exchange-graphcache
import { cacheExchange } from '@urql/exchange-graphcache';

const cache = cacheExchange({
  optimistic: {
    addTodo: (args, cache, info) => ({
      __typename: 'Todo',
      id: 'temp-' + Date.now(),
      text: args.text,
      done: false,
    }),
  },
  updates: {
    Mutation: {
      addTodo: (result, args, cache) => {
        cache.invalidate('Query', 'todos');
      },
    },
  },
});

// ===== Relay =====
// Uses updater functions on commitMutation:
commitMutation(environment, {
  mutation: AddTodoMutation,
  variables: { text },
  optimisticUpdater: (store) => {
    const todo = store.create('temp-' + Date.now(), 'Todo');
    todo.setValue(text, 'text');
    todo.setValue(false, 'done');
    const root = store.getRoot();
    const todos = root.getLinkedRecords('todos') || [];
    root.setLinkedRecords([...todos, todo], 'todos');
  },
  updater: (store) => { /* real update from server response */ },
});

// ===== Pattern: rollback on error =====
// Apollo + urql roll back automatically if the mutation errors.
// Show a toast: 'Failed to add'; the optimistic record reverts.

// ===== Caveats =====
// - Server may return a different shape than predicted -> reconcile carefully
// - Generated IDs (UUIDs) on client -> sometimes the server returns a different ID
// - Realtime subscriptions add 'real' record before mutation returns -> dedup

// ===== When optimistic helps =====
// - Toggles (like, follow, mark-as-read)
// - Add to list (todo, comment)
// - Reordering / drag-and-drop
// - Form save in a stable UI

// ===== When to skip =====
// - Mutations that change critical balances (payment) -> wait
// - Mutations the user must see fail before retrying (validation-heavy)
// - Cross-entity coordinated changes (saga-style)

// ===== Patterns =====
// - Predict the EXPECTED shape with __typename
// - Use cache.modify / cache.writeFragment in Apollo
// - Show pending state subtly (spinner, opacity)
// - Toast on error; auto-rollback handles UI

// ===== Pitfalls =====
// - Optimistic with subscriptions -> double records
// - Server returning different IDs without remapping
// - Optimistic without rollback -> stale UI on error
// - Predicting too much (status changes that depend on server-side logic)

Why it matters

Optimistic updates make slow networks feel fast. Apollo cache.modify, urql graphCache optimistic, Relay optimisticUpdater. Predict the shape with __typename, reconcile when the server responds, roll back on error. Toggles + add-to-list are the obvious wins; payments and saga-style flows are not.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
client.mutate({ mutation, optimisticResponse: { ... }, update(cache) { ... } });
Try it Yourself »

Discussion

Loading…