Padrões React (React Patterns)
Padrões idiomáticos React 18/19 para construir árvores de componentes robustas, acessíveis e performáticas.
Quando Ativar
- Escrever ou modificar componentes funcionais React, hooks customizados ou árvores de componentes
- Revisar arquivos JSX/TSX
- Projetar forma do estado ou composição de componentes
- Migrar componentes de classe ou código antigo com
forwardRef/useEffect - Escolher entre estado local, estado levantado, contexto e stores externas
- Trabalhar com Server Components / Client Components (Next.js App Router, RSC)
- Implementar formulários com ações do React 19 ou inputs controlados
- Conectar busca de dados com TanStack Query / SWR / RSC
Princípios Fundamentais
1. Render é uma Função Pura de Props e Estado
// Bom: deriva durante o render
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 render, pode dessincronizar e obscurece o fluxo de dados.
2. Efeitos Colaterais Fora do Render
Efeitos, mutações, chamadas de rede e inscrições vivem em manipuladores de eventos ou useEffect — nunca no corpo do render.
3. Composição Sobre Herança
React não tem modelo de herança para componentes. Compõe com children, render props ou props de componente.
Disciplina de Hooks
Consulte rules/react/hooks.md para o conjunto completo de regras. Destaques:
- Apenas no nível superior, nunca condicional
- Limpar toda inscrição, intervalo, listener
- Atualizador funcional (
setX(prev => prev + 1)) quando o novo estado depende do anterior - Posição padrão: não memoizar — adicione
useMemo/useCallbackapenas quando um profiler ou uma cadeia de dependências provar que é necessário - Extrair um hook customizado apenas quando a mesma sequência de hooks aparece em 2+ componentes
Árvore de Decisão para Localização do Estado
Usado por um componente?
-> useState dentro dele
Usado pelo pai + alguns descendentes?
-> levantar para o 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, RSC fetch)
A maioria das páginas não precisa de contexto ou store global. Resista à abstração até que levantar estado se torne doloroso.
Componentes Servidor / Cliente (RSC)
// Componente Servidor - padrão, assíncrono, nunca envia JavaScript 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} />;
}
// Componente Cliente - opt-in 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:
- Servidor -> Cliente: passar props serializáveis ou
children - Cliente -> Servidor: invocar Server Actions via
<form action={...}>ou imperativamente a partir de manipuladores de eventos - Nunca
importum Componente Servidor de um arquivo de Componente Cliente — compõe viachildren
Suspense + Error Boundaries
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
- Coloque 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 render, ciclo de vida e construtores de seus filhos — NÃO em manipuladores de eventos ou código assíncrono
Formulários
Ações 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 controle quando o valor dirige outra UI, formata a cada tecla ou implementa validação em tempo real.
Formulários complexos
Para formulários de múltiplas etapas, 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 simplicidade trivial é uma armadilha de manutenção.
Matriz de Decisão para Busca de Dados
| Necessidade | Ferramenta |
|---|---|
| Dados por requisição no Next.js App Router | RSC await fetch() |
| Cache + mutações + invalidação no cliente | TanStack Query |
| Cache leve + revalidação no cliente | SWR |
| Inscrições em tempo real | Server-Sent Events, WebSockets ou API de inscrição da biblioteca |
| Única vez e esquece | fetch() em um manipulador de eventos |
Evite useEffect + fetch para dados de aplicação — condições de corrida, sem cache, sem retentativa, 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.
Performance
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 renders
- Seu render é mensuravelmente caro
React.memo adiciona uma verificação de igualdade em cada render. Se as props diferirem na maioria dos renders, a verificação é pura sobrecarga.
Evitando Cascatas de Render
- Levante o estado para baixo em vez de para cima, quando possível
- Divida o 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 externas — 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-windowassim que 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 alcançável por teclado
- Inputs de formulário precisam de labels —
<label htmlFor>ouaria-labelse visualmente rotulado por um ícone - Gerencie o foco em mudanças de rota e abertura/fechamento de modal
- Execute
axeem testes de componentes (consulte skills/react-testing) - Link cruzado: skills/accessibility/SKILL.md cobre critérios WCAG e bibliotecas de padrões
Roteamento
Este skill é independente de roteador. 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 Ponteiro)
- Especificidades do Next.js: Carregamento de dados do App Router, Route Handlers, Middleware, Rotas Paralelas — 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 seguem a documentação do Remix
Relacionados
- Regras: rules/react/ — estilo de código, hooks, padrões, segurança, teste
- Skills: react-performance para o conjunto de regras de performance 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 customizado 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 React 19 useOptimistic
"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: "eu" });
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 render
// Dois contextos: um raramente muda, o outro frequentemente
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);
// Um componente que consome apenas ThemeContext NÃO re-renderiza quando as notificações mudam