Padrões React
Padrões idiomáticos React 18/19 para construir árvores de componentes robustas, acessíveis e de alto desempenho.
Quando Ativar
- Escrevendo ou modificando componentes de função React, hooks personalizados ou árvores de componentes
- Revisando arquivos JSX/TSX
- Projetando forma de estado ou composição de componentes
- Migrando componentes de classe ou código antigo com
forwardRef/useEffectpesado - Escolhendo entre estado local, estado elevado, contexto e stores externas
- Trabalhando com Server Components / Client Components (Next.js App Router, RSC)
- Implementando formulários com actions do React 19 ou inputs controlados
- Integrando busca de dados com TanStack Query / SWR / RSC
Princípios Centrais
1. Renderização é uma Função Pura de Props e Estado
// Bom: deriva durante a renderização
function Cart({ items }: { items: CartItem[] }) {
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
return <span>{formatMoney(total)}</span>;
}
// Ruim: estado derivado armazenado separadamente
function Cart({ items }: { items: CartItem[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
return <span>{formatMoney(total)}</span>;
}
Estado derivado em useEffect adiciona um ciclo de renderização, pode dessincronizar e obscurece o fluxo de dados.
2. Efeitos Colaterais Fora da Renderização
Efeitos, mutações, chamadas de rede e inscrições vivem em manipuladores de eventos ou useEffect — nunca no corpo da renderização.
3. Composição Sobre Herança
React não tem modelo de herança para componentes. Componha com children, render props ou props de componente.
Disciplina de Hooks
Veja rules/react/hooks.md para o conjunto completo de regras. Destaques:
- Apenas no nível superior, nunca condicionais
- Limpeza de toda inscrição, intervalo, listener
- Atualizador funcional (
setX(prev => prev + 1)) quando novo estado depende do anterior - Posição padrão: não memorizar — adicione
useMemo/useCallbackapenas quando um profiler ou uma cadeia de dependências provar que é necessário - Extraia um hook personalizado apenas quando a mesma sequência de hooks aparecer em 2+ componentes
Árvore de Decisão de Localização de Estado
Usado por um único componente?
-> useState dentro dele
Usado pelo pai + alguns descendentes?
-> elevar ao ancestral comum mais próximo
Usado em ramos distantes E leituras de baixa frequência (tema, autenticação, localidade)?
-> React Context
Atualizações de alta frequência compartilhadas pela árvore?
-> store externa (Zustand, Jotai, Redux Toolkit)
Derivado de um servidor?
-> biblioteca de estado do servidor (TanStack Query, SWR, fetch RSC)
A maioria das páginas não precisa de contexto ou store global. Resista à abstração até que a elevação duplicada se torne dolorosa.
Server / Client Components (RSC)
// Server Component - padrão, assíncrono, nunca envia JS para si mesmo
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) notFound();
return <ProductView product={product} />;
}
// Client Component - opte com "use client"
"use client";
export function AddToCartButton({ productId }: { productId: string }) {
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => startTransition(() => addToCart(productId))}
>
{pending ? "Adicionando..." : "Adicionar ao carrinho"}
</button>
);
}
Limites:
- Server -> Client: passe props serializáveis ou
children - Client -> Server: invoque Server Actions via
<form action={...}>ou imperativamente a partir de manipuladores de eventos - Nunca
importum Server Component de um arquivo Client Component — componha-os viachildren
Suspense + Error Boundaries
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
- Coloque os limites Suspense próximos aos dados, não na raiz da rota — revele conteúdo progressivamente
- Error Boundary continua sendo uma API de classe; use
react-error-boundarypara um wrapper amigável a hooks - Um boundary captura erros lançados durante a renderização, ciclo de vida e construtores de seus filhos — NÃO em manipuladores de eventos ou código assíncrono
Formulários
Actions de formulário do React 19 (preferido para código novo)
"use client";
import { useActionState } from "react";
const initial = { error: null as string | null };
async function updateUserAction(_prev: typeof initial, formData: FormData) {
"use server";
const parsed = UserSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: "Entrada inválida" };
await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
return { error: null };
}
export function UserForm() {
const [state, formAction, pending] = useActionState(updateUserAction, initial);
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={pending}>Salvar</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
Inputs controlados
Use controlado quando o valor direciona outra UI, formata a cada tecla, ou implementa validação em tempo real.
Formulários complexos
Para formulários multi-etapas, arrays de campos dinâmicos ou validação entre campos: use uma biblioteca (React Hook Form, TanStack Form). Gerenciar estado manualmente para formulários além da complexidade trivial é uma armadilha de manutenção.
Matriz de Decisão de Busca de Dados
| Necessidade | Ferramenta |
|---|---|
| Dados por requisição no Next.js App Router | RSC await fetch() |
| Cache do lado do cliente + mutações + invalidação | TanStack Query |
| Cache cliente leve + revalidação | SWR |
| Inscrições em tempo real | Server-Sent Events, WebSockets, ou a API de inscrição da lib |
| Único disparo e esquece | fetch() em um manipulador de evento |
Evite useEffect + fetch para dados de aplicação — condições de corrida, sem cache, sem repetição, sem integração com Suspense.
Receitas de Composição
Slot via children
<Layout>
<Header />
<Main>{content}</Main>
</Layout>
Slots nomeados
<Page header={<Nav />} sidebar={<Filters />}>
<Results />
</Page>
Componentes compostos (estado compartilhado via Context)
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Perfil</Tabs.Trigger>
<Tabs.Trigger value="settings">Configurações</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><Profile /></Tabs.Panel>
<Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>
Render prop / função como filho
Útil quando o pai precisa passar parâmetros para a saída renderizada:
<DataLoader id={id}>
{({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
Alternativa moderna: um hook (useData(id)) retornando a mesma forma — geralmente mais limpo.
Desempenho
Quando React.memo Realmente Ajuda
Envolva um componente em React.memo apenas quando:
- Ele re-renderiza frequentemente
- Suas props são geralmente as mesmas entre renderizações
- Sua renderização é mensuravelmente cara
React.memo adiciona uma verificação de igualdade em cada renderização. Se as props diferirem na maioria das renderizações, a verificação é pura sobrecarga.
Evitando Cascatas de Renderização
- Eleve o estado para baixo em vez de para cima quando possível
- Divida contexto: um contexto por preocupação, para que uma mudança em
themeContextnão re-renderize consumidores de autenticação - Use
useSyncExternalStorepara bibliotecas de estado externo — necessário para renderização concorrente segura
Listas
- Forneça props
keyestáveis (id do banco de dados, não índice do array) - Virtualize listas longas com
@tanstack/react-virtualoureact-windowquando a contagem de itens visíveis exceder ~50 com linhas não triviais
Composição com Acessibilidade em Primeiro Lugar
- Sempre renderize HTML semântico (
<button>,<a>,<nav>,<main>) antes de recorrer a atributosrole - Todo elemento interativo deve ser acessível por teclado
- Inputs de formulário precisam de labels —
<label htmlFor>ouaria-labelse rotulado visualmente por um ícone - Gerencie o foco em mudanças de rota e abertura/fechamento de modal
- Execute
axeem testes de componente (veja skills/react-testing) - Link cruzado: skills/accessibility/SKILL.md cobre critérios WCAG e bibliotecas de padrões
Roteamento
Este skill é agnóstico a roteadores. Os padrões acima funcionam com React Router, TanStack Router, Next.js App Router, Remix Router. Padrões específicos de roteador (loaders, actions, layouts aninhados) seguem a documentação do roteador — são preocupações de framework sobrepostas ao núcleo do React.
Fora do Escopo (Seções de Referência)
- Especificidades do Next.js: Carregamento de dados do App Router, Route Handlers, Middleware, Parallel Routes — preocupação separada, use a documentação do Next.js
- React Native: Padrões específicos de plataforma diferem o suficiente para justificar um skill separado
react-native-patterns(ainda não presente) - Remix: Convenções de loader/action se sobrepõem ao RSC, mas sigam a documentação do Remix
Relacionados
- Regras: rules/react/ — estilo de código, hooks, padrões, segurança, testes
- Skills: react-performance para o conjunto de regras de desempenho derivado da Vercel, frontend-patterns para preocupações de UI entre frameworks, accessibility, angular-developer para comparação de frameworks
- Agentes:
react-reviewerpara revisão de código,react-build-resolverpara erros de build/bundler - Comandos:
/react-review,/react-build,/react-test
Exemplos
Hook personalizado para busca com debounce
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState("");
const debounced = useDebounce(query, 300);
const { data } = useQuery({
queryKey: ["search", debounced],
queryFn: () => searchApi(debounced),
enabled: debounced.length > 0,
});
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results items={data ?? []} />
</>
);
}
UI Otimista com useOptimistic do React 19
"use client";
import { useOptimistic } from "react";
export function MessageList({ messages }: { messages: Message[] }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage],
);
async function send(formData: FormData) {
const text = String(formData.get("text"));
addOptimistic({ id: "pending", text, sender: "me" });
await saveMessage(text);
}
return (
<>
<ul>{optimistic.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
<form action={send}>
<input name="text" />
<button type="submit">Enviar</button>
</form>
</>
);
}
Dividindo contexto para evitar cascatas de renderização
// Dois contextos: um raramente muda, outro frequentemente
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);
// Um componente que consome apenas ThemeContext NÃO re-renderiza quando notificações mudam