SuperAPI AI Directory

tanstack-query-intent-core-concurrent-optimistic-updates

General & other

View repository

Updated 10-16 00:02

About this skill

Core Patterns

Concurrent optimistic updates need per-mutation identity and scoped invalidation. A rollback or invalidation from one mutation should not erase another mutation that is still pending.

Scope related mutations

const mutation = useMutation({
  mutationKey: ['items'],
  mutationFn: toggleItem,
  onMutate: async ({ id }) => {
    await queryClient.cancelQueries({ queryKey: ['items', 'detail', id] })
    const previousItem = queryClient.getQueryData<Item>(['items', 'detail', id])

    queryClient.setQueryData<Item>(['items', 'detail', id], (item) =>
      item ? { ...item, isActive: !item.isActive } : item,
    )
return { previousItem }

}, onError: (_error, variables, context) => { queryClient.setQueryData( ['items', 'detail', variables.id], context?.previousItem, ) }, onSettled: () => { if (queryClient.isMutating({ mutationKey: ['items'] }) === 1) { return queryClient.invalidateQueries({ queryKey: ['items'] }) } }, })


Use `queryClient.isMutating()` imperatively inside the callback so the count is current at the moment invalidation would run.

### Render pending variables with stable keys

```tsx
const pendingAdds = useMutationState<string>({
  filters: { mutationKey: ['todos', 'add'], status: 'pending' },
  select: (mutation) =>
    `${mutation.state.submittedAt}:${mutation.state.variables}`,
})

submittedAt distinguishes multiple pending mutations with the same variables.

Common Mistakes

CRITICAL One mutation invalidation reverts another optimistic update

Wrong:

onSettled: () => queryClient.invalidateQueries({ queryKey: ['items'] })

Correct:

onSettled: () => {
  if (queryClient.isMutating({ mutationKey: ['items'] }) === 1) {
    return queryClient.invalidateQueries({ queryKey: ['items'] })
  }
}

Skip intermediate invalidations while related optimistic mutations are still in flight.

Source: https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query

HIGH Optimistic list update ignores current filters

Wrong:

queryClient.setQueryData(['items', 'list', filters], (items) =>
  items?.map((item) => (item.id === updated.id ? updated : item)),
)

Correct:

queryClient.setQueryData(['items', 'list', filters], (items) =>
  items
    ?.map((item) => (item.id === updated.id ? updated : item))
    .filter((item) => matchesFilters(item, filters)),
)

If the server would remove the updated item from the filtered list, the optimistic cache update should do the same.

Source: https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query

HIGH Pending optimistic rows share unstable keys

Wrong:

{
  variables.map((title) => <li key={title}>{title}</li>)
}

Correct:

const pending = useMutationState({
  filters: { mutationKey: ['todos', 'add'], status: 'pending' },
  select: (mutation) => ({
    variables: mutation.state.variables,
    submittedAt: mutation.state.submittedAt,
  }),
})

{
  pending.map((todo) => <li key={todo.submittedAt}>{todo.variables.title}</li>)
}

Use mutation metadata to keep concurrent optimistic rows distinct.

Source: TanStack/query:docs/framework/react/guides/optimistic-updates.md