hardReact Native#15

Optimistic updates with React Query

Prompt

Implement optimistic updates with React Query for a "like" toggle on a post. Update the cache immediately, then revert on API failure.

Solution

const queryClient = useQueryClient()
const mutation = useMutation({
  mutationFn: (id) => fetch(`/api/posts/${id}/like`, { method: 'POST' }),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['post', id] })
    const previous = queryClient.getQueryData(['post', id])
    queryClient.setQueryData(['post', id], (old) => ({ ...old, liked: !old.liked }))
    return { previous }
  },
  onError: (err, id, context) => {
    queryClient.setQueryData(['post', id], context.previous)
  },
  onSettled: (data, err, id) => {
    queryClient.invalidateQueries({ queryKey: ['post', id] })
  },
})
Mentor's take

Optimistic updates buy perceived latency: on a mobile network a like round-trip can take a second, and a button that doesn't respond for a second reads as broken. The pattern assumes success, updates the cache immediately, and treats failure as the exceptional path to roll back.

The choreography is a three-beat contract, and each beat exists for a specific race:

  1. onMutate: cancel, snapshot, write. cancelQueries first — if a background refetch for this key is already in flight, its response would land after your optimistic write and overwrite it with pre-mutation data. Then snapshot previous and write the optimistic value.
  2. onError: restore the snapshot. The context returned from onMutate carries previous to the error handler — that return value is the designed hand-off, not a convenience.
  3. onSettled: invalidate. Success or failure, refetch the key so the cache converges on the server's truth — your optimistic guess and the server's result (like counts, timestamps) can differ even on success.

Red flag: skipping cancelQueries. The demo works, then in production a refetch races the optimistic write and users watch their like flicker off. Knowing why the cancel is there is the senior tell.

Say it: "Optimistic UI is cancel, snapshot, write, then roll back on error via the onMutate context — and always invalidate on settle so the cache reconverges with the server."