Capítulo 48 de 456
Guia passo a passo (app "Taskboard") para dar feedback responsivo a interações que dependem de trabalho server-side: streaming de leituras lentas, toggles instantâneos, filtros com pending state, comentários otimistas, drag-and-drop, formulários com useActionState, e cache com invalidação precisa para tornar navegação repetida instantânea.
<Suspense> com promise inline: envolver params.then(...) diretamente no JSX mantém a página síncrona e permite streaming por seção sem bloquear no fetch mais lento.useOptimistic(value): fornece um valor otimista para renderizar no lugar da prop stale enquanto uma transição está pendente; reverte ao valor real quando a transição termina.useTransition() / startTransition(): roda a Server Function como parte de uma transição; erros lançados dentro são encaminhados ao error boundary mais próximo sem try/catch manual.xAction/changeAction: prop nomeada com sufixo Action sinaliza que o componente consumidor deve rodá-la dentro de sua própria transição (padrão do React para "Exposing action props from components").data-pending attribute + :has(): componente filho expõe estado pendente via atributo de dado; ancestral estiliza com group-has-data-pending: ou has-data-pending: sem precisar de callback/lifting state. Evitar em interações de alta frequência (drag, scroll) porque o browser reavalia :has() na subtree a cada toggle.useOptimistic([]) para listas pendentes: separa a lista persistida (server component) da lista de itens ainda não confirmados (client component), reconciliadas quando o server render fresco chega.formRef.current?.reset(): dentro de uma transição, useState setters são adiados até o fim da transição, mas manipulação direta do DOM (como reset de formulário) e setters de useOptimistic aplicam no frame atual.useOptimistic com reducer: recebe a lista completa e uma action, remapeando um item específico; se um refresh em background chegar no meio (polling, mutação de outro usuário), React reaplica o reducer sobre os dados atualizados.useActionState(action, initialState): gerencia isPending do botão, reset de campos via key incremental, e roda a action já dentro de uma transição.await não é transição automaticamente: dentro de uma action de useActionState, atualizações de estado depois do await (ex.: setIsOpen(false)) precisam ser envolvidas manualmente em startTransition para serem batchadas com o resto.refresh(): reroda trabalho dinâmico mas deixa entradas cacheadas intactas; usar para mutações em dados que permanecem dinâmicos (ex.: thread de comentários).updateTag(tag): invalida entradas cacheadas por tag após uma escrita, substituindo refresh() quando a leitura já está sob 'use cache' + cacheTag.// features/task/components/task-card.tsx — toggle otimista
'use client'
import { useOptimistic, useTransition } from 'react'
import { cyclePriority } from '@/features/task/task-actions'
import { PRIORITY_CYCLE } from '@/lib/data'
export function TaskCard({ id, priority }) {
const [optimisticPriority, setOptimisticPriority] = useOptimistic(priority)
const [, startTransition] = useTransition()
function handlePriority() {
startTransition(async () => {
setOptimisticPriority(PRIORITY_CYCLE[optimisticPriority])
await cyclePriority(id)
})
}
return (
<button onClick={handlePriority} className={priorityDot[optimisticPriority]}>
{optimisticPriority}
</button>
)
}
useOptimistic + useTransition para atualizar UI no frame atual sem esperar a Server Function.// features/task/components/board.tsx — drag-and-drop com reducer otimista
const [optimisticTasks, moveTask] = useOptimistic(
tasks,
(currentTasks, action: { taskId: string; status: Status }) =>
currentTasks.map((t) => (t.id === action.taskId ? { ...t, status: action.status } : t))
)
function handleDrop(targetStatus, taskId) {
startTransition(async () => {
moveTask({ taskId, status: targetStatus })
const result = await updateStatus(taskId, targetStatus)
if (!result.success) toast.error(result.error)
})
}
useOptimistic com reducer para remapear um item numa lista, usando startTransition puro (não useTransition) para não disparar isPending indesejado.// features/task/task-queries.ts — leitura cacheada e invalidação
import { cacheLife, cacheTag } from 'next/cache'
import { getTaskById } from '@/lib/db'
export async function getTask(id: string) {
'use cache'
cacheLife('hours')
cacheTag('tasks', `task-${id}`)
return getTaskById(id)
}
// features/task/task-actions.ts
'use server'
import { updateTag } from 'next/cache'
export async function updateStatus(taskId: string, newStatus: Status) {
const updated = await updateTaskStatus(taskId, newStatus)
if (!updated) return { success: false as const, error: 'Task no longer exists' }
updateTag('tasks')
updateTag(`task-${taskId}`)
return { success: true as const, status: newStatus }
}
cacheTag) e invalidação precisa (updateTag) substituindo refresh() para deixar navegações repetidas instantâneas.| Situação | Use |
|---|---|
| Dado lento deve fazer stream sem bloquear a página | <Suspense> |
| Um valor deve atualizar enquanto trabalho assíncrono roda | useOptimistic |
| Trabalho assíncrono precisa de pending state, erro, ou UI coordenada | useTransition |
| Formulário precisa de pending, reset e result state | useActionState |
| Ancestral deve mostrar pending state de trabalho acontecendo em outro lugar | atributo data-pending estilizado via CSS |
| Leituras reutilizáveis devem sobreviver entre requests e ficar frescas após writes | 'use cache' + cacheTag, revalidado por updateTag ou revalidateTag |
| Navegação entre páginas deve parecer instantânea | <Link> prefetching (Partial Prefetching) + prefetch={true} para conteúdo dependente de URL |
onClick sem useOptimistic/useTransition: a UI continua renderizando a prop antiga até o próximo server render chegar.useState para pending state dentro de uma action de formulário: setIsOpen(false) fora de transição fecha o diálogo antes do board atualizar, produzindo dessincronia visual de um frame.useTransition().isPending quando o feedback visual já vem do próprio valor otimista: no drag-and-drop, isso dispara o mesmo fade usado por outro pending state (filtro), causando comportamento indesejado; prefira startTransition isolado.:has() (data-pending) em interações de alta frequência: dragging/scrolling recalculando a subtree inteira a cada toggle é caro; use client state nesses casos.useOptimistic some ao fim da transição e é substituído pelo valor real vindo do próximo server render, não é um estado permanente.await, sem precisar de startTransition.xAction comunica ao consumidor que a função deve ser chamada dentro de uma transição.refresh() é para dados que permanecem dinâmicos; updateTag é para dados cacheados que precisam de invalidação cirúrgica.cacheComponents: true) é pré-requisito só do Step 8 (navegação instantânea); os Steps 1-7 funcionam sem ele.<Suspense> usados no Step 1.