R
Interview Deckreact · next.js · senior/staff
0·0·122
System Design

Optimistic UI — Resposta Instantânea com Rollback

Atualizar UI antes da resposta do servidor. useOptimistic do React 19, rollback em erro.

Optimistic UI atualiza a interface imediatamente ao receber a ação do usuário, sem esperar a confirmação do servidor. Se o servidor falhar, a UI faz rollback para o estado anterior. Cria a percepção de resposta instantânea. Usado extensivamente em: likes, follows, reordenar listas (drag-and-drop), send de mensagens, toggle de switches.

Problema
UI que congela esperando confirmação do servidor parece lenta mesmo com boa infraestrutura. Botão de 'curtir' que demora 300ms para responder causa má experiência quando a operação é trivial.
Solução
useOptimistic (React 19) é a forma canônica. Para React 18: setar estado local imediatamente, fazer o request em paralelo, em caso de erro reverter com setQueryData do React Query ou restaurar estado local. O truque: guardar o estado anterior antes de atualizar.
Tip
Nem toda ação deve ser optimistic. Regra: operação é reversível + falha é rara + latência importa → optimistic. Operações financeiras (pagamento), deletar dados permanentemente, ou operações com dependências → esperar confirmação do servidor. O Uber usa optimistic para curtir um motorista mas não para solicitar a corrida.
// React 19 com useOptimistic
import { useOptimistic, useTransition } from 'react'

function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)
  const [isPending, startTransition] = useTransition()

  const handleLike = async () => {
    startTransition(async () => {
      setOptimisticLikes(prev => prev + 1) // atualiza UI imediatamente
      try {
        await likePost(postId) // request ao servidor
      } catch {
        // useOptimistic reverte automaticamente em caso de erro
      }
    })
  }

  return (
    <button onClick={handleLike} disabled={isPending}>
      ❤️ {optimisticLikes}
    </button>
  )
}

// React 18 com React Query — padrão manual
const mutation = useMutation({
  mutationFn: (id: string) => likePost(id),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['post', id] })
    const prev = queryClient.getQueryData(['post', id]) // snapshot
    queryClient.setQueryData(['post', id], (old: Post) => ({
      ...old, likes: old.likes + 1 // optimistic
    }))
    return { prev } // contexto para rollback
  },
  onError: (err, id, context) => {
    queryClient.setQueryData(['post', id], context?.prev) // rollback
  },
  onSettled: (_, __, id) => {
    queryClient.invalidateQueries({ queryKey: ['post', id] }) // refetch
  },
})
Q.Como implementar rollback confiável?
A.Guardar snapshot do estado anterior: `const prev = queryClient.getQueryData(key)`. No catch da mutation: `queryClient.setQueryData(key, prev)`. Com React Query, useMutation tem callbacks onMutate (salva prev, aplica optimistic), onError (rollback), onSettled (invalida query para buscar dado real). O useOptimistic do React 19 faz isso automaticamente.
Q.O que fazer quando o servidor retorna diferente do optimistic?
A.Na onSettled ou onSuccess, invalidar a query para refetch do dado real: `queryClient.invalidateQueries({ queryKey: key })`. O dado optimistic é substituído pelo dado real. Se o servidor enriquece o dado (ex: adiciona um campo `id` ao item criado), o optimistic temporário some e o real aparece — pode causar um flash visual. Solução: usar um id temporário (uuid) que é substituído pelo id real.
Q.Optimistic UI e conflito de edições concorrentes (dois usuários editam o mesmo item)?
A.Problema clássico de consistency. Abordagens: Last-write-wins (mais simples, pode perder dados), OT (Operational Transformation, Google Docs), CRDT (Conflict-free Replicated Data Types, Figma, Notion). Para maioria dos apps: last-write-wins com timestamp é suficiente. Para colaboração em tempo real em documentos: CRDT com bibliotecas como Yjs ou Automerge.
117/122← →