Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-04
BloqueadoA maior parte dos devs React opera num modelo mental simplificado: "componente é função, hooks são state". Funciona pra fazer features triviais. Quebra na hora de:
useMemo e useState.key é crítica (e por que index como key dá bug sutil).A diferença entre Pleno e Senior em React é profundidade do modelo: como o reconciler decide o que re-renderizar, o que Fiber permite, o que mudou com Concurrent Mode, o que RSC realmente é. Sem isso, você é refém do framework.
A premissa do React: você descreve o que deve aparecer dado um estado, não como chegar lá. Quando o estado muda, React reconcilia (descobre o diff) e atualiza o DOM minimamente.
Esse é um dos paradigmas mais importantes do frontend moderno, e tem custo. React precisa comparar estado anterior vs novo pra calcular o diff. Esse processo é a "reconciliação", e é onde a maior parte das otimizações vive.
"Virtual DOM" é o nome popular pra: representação em memória da árvore de UI. Em React, isso são objetos JS chamados elementos (criados via JSX → React.createElement → objetos { type, props, key, ref }).
Em cada render, sua função componente retorna uma nova árvore de elements. React compara com a árvore anterior, calcula o diff, e aplica ao DOM real (ou a outro renderer, React Native, Ink, etc.).
A palavra "virtual" sugere mágica que não existe. É só representação intermediária.
Fiber é a estrutura interna que React mantém pra cada nó da árvore. Cada Fiber contém:
type, key, props, statechild, sibling, return (parent), linked treealternate, versão "atual" vs "work-in-progress"A linked tree em vez de tree-recursivo permite interrupção e retomada: React pode pausar a meio de um render se algo mais prioritário chega (input do usuário, p.ex.), e voltar depois. Isso é a base do Concurrent Mode.
Fases do trabalho:
Por isso effects rodam depois do paint, e por isso useState setter durante render é problemático (você está dentro da render phase, mexendo em state que React ainda não terminou de processar).
Algoritmo de diff é heurístico, O(n) (vs O(n³) ótimo). Premissas:
key pra identificar mesma "entidade lógica" entre renders.Sem key (ou com index como key) em lista mutável, React assume "mesmo elemento na mesma posição", bug clássico: você reordena, mas state interno (input value, focus) acompanha o índice, não o item.
Use key estável e única do domínio (order.id, não index). Index como key só está OK em listas estáticas que nunca reordenam.
Um componente re-renderiza quando:
setX).useState, useReducer triggeram. setState(v) agenda render. Múltiplos setState dentro do mesmo handler/effect são batched (um único render).
React.memo(Component) pula re-render se props (shallow comparison) não mudaram. Atalho que parece grátis mas pode não ser:
Hooks dependem de ordem de chamada estável. Internamente, React mantém uma linked list de hooks por componente. Cada useState é uma "slot" na lista. Por isso:
useState:
useState(() => expensive()), função roda só no mount.setX(prev => prev + 1), evita stale closures.useEffect:
[] é "uma vez no mount", [a, b] é "quando a ou b mudam".useLayoutEffect:
useEffect (não bloqueia paint).useMemo, useCallback:
useMemo(() => compute(), [deps]), pra cálculo caro ou referência estável (passar pra React.memo filho).useCallback(fn, [deps]), açúcar pra useMemo(() => fn, [deps]).useRef:
ref.current é reescrevível sem re-render.<div ref={ref}>), valor mutável que não dispara render (timers, last value, cancellation tokens).useReducer:
useContext:
useTransition, useDeferredValue (Concurrent):
useSyncExternalStore, para integrar stores externos (Redux, Zustand) com Concurrent Mode corretamente.
React Compiler (RC 2024, GA 2025-2026) automatiza memoization estática. Vale entender o modelo mental novo porque muda profundamente como você escreve componentes.
O que ele faz:
useMemo chains).Antes do compiler:
const Card = memo(function Card({ user, onSelect }) {
const fullName = useMemo(() => `${user.first} ${user.last}`, [user.first, user.last]);
const handleClick = useCallback(() => onSelect(user.id), [onSelect, user.id]);
return <button onClick={handleClick}>{fullName}</button>;
});
Com compiler:
function Card({ user, onSelect }) {
const fullName = `${user.first} ${user.last}`;
return <button onClick={() => onSelect(user.id)}>{fullName}</button>;
}
Compiler detecta que fullName depende só de user.first/.last, que onClick depende de onSelect/user.id, e gera memoization equivalente. Sem React.memo, sem useMemo, sem useCallback. O código fica como você escreveria sem otimização, performance vem grátis.
Rules of React (compiler-friendly):
eslint-plugin-react-compiler (build-time) avisa quando código viola e o compiler vai pular esse arquivo (bail-out).
Bail-out behavior:
Migrações práticas:
useMemo em massa. Compiler é resiliente, manter useMemos antigos não quebra.React.memo redundante depois de auditar, compiler memoiza componentes shallow-equal-prop automaticamente.react-compiler/cannot-be-compiled flagsa razão exata da bail-out.Quando ainda escrever useMemo manualmente:
O que muda em interview/review:
useMemo/useCallback em código novo é sinal de "não confia no compiler", questione, não copie.Suspense é mecanismo pra componentes esperarem algo (data, código lazy-loaded) sem você precisar gerenciar loading state manualmente.
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Quando algo dentro de Comments "throw a Promise" (convenção), React intercepta, mostra fallback, e re-tenta render quando promise resolve.
Use cases:
lazy() pra code splitting: const Comments = lazy(() => import('./Comments')).React Query com suspense: true, RSC, Relay).Streaming SSR em React 18+: o servidor manda HTML em pedaços conforme suspense boundaries resolvem. Cliente hydrata progressivamente. UX melhor (TTFB rápido, conteúdo não-crítico depois).
Suspense sozinho captura pending; Error Boundary captura rejected. Pareados, são contrato completo de async UI. Em produção, app sem error boundary leva white screen no primeiro erro de fetch — inaceitável.
Hierarquia de boundaries pra Logística dashboard:
// app/dashboard/page.tsx — granular boundaries por widget
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
{/* Cada widget tem own boundary — falha localizada não derruba página */}
<Widget title="Pedidos hoje">
<ErrorBoundary FallbackComponent={WidgetErrorFallback} onReset={() => /* refetch */}>
<Suspense fallback={<WidgetSkeleton />}>
<OrdersTodayCount />
</Suspense>
</ErrorBoundary>
</Widget>
<Widget title="Couriers ativos">
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<Suspense fallback={<WidgetSkeleton />}>
<ActiveCouriers />
</Suspense>
</ErrorBoundary>
</Widget>
<Widget title="Receita semanal">
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<Suspense fallback={<ChartSkeleton />}>
<WeeklyRevenue />
</Suspense>
</ErrorBoundary>
</Widget>
</div>
);
}
function WidgetErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div role="alert" className="p-4 border-red-300 rounded">
<p className="text-red-700">Não foi possível carregar este painel.</p>
<button onClick={resetErrorBoundary} className="text-blue-600 underline">
Tentar novamente
</button>
{process.env.NODE_ENV === 'development' && (
<pre className="text-xs mt-2">{error.message}</pre>
)}
</div>
);
}
Retry com React Query reset (pattern canônico):
'use client';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
export function DashboardWithReset({ children }: { children: React.ReactNode }) {
const { reset } = useQueryErrorResetBoundary();
return (
<ErrorBoundary
onReset={reset} // limpa cache + dispara refetch
FallbackComponent={WidgetErrorFallback}
>
<Suspense fallback={<DashboardSkeleton />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
useTransition marca update como non-urgent: usuário pode clicar em outra coisa enquanto componente carrega. Não trava input.
'use client';
import { useTransition, useState } from 'react';
export function TenantSwitcher({ tenants }: { tenants: Tenant[] }) {
const [selectedId, setSelectedId] = useState(tenants[0].id);
const [isPending, startTransition] = useTransition();
return (
<>
<select
value={selectedId}
onChange={(e) => {
startTransition(() => setSelectedId(e.target.value));
}}
disabled={false} // não bloqueia mesmo durante pending
>
{tenants.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
<span className={isPending ? 'opacity-50' : ''}>
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard tenantId={selectedId} />
</Suspense>
</span>
</>
);
}
Sem transition: select trava 200-800ms enquanto Dashboard carrega. Com transition: select responde imediato; Dashboard fica "stale + dim" até next data chegar.
useDeferredValue — defer derivação caraDiferente de transition (que defere setState), useDeferredValue pega valor já-mudado e atrasa render do dependente:
function SearchableOrderList({ search }: { search: string }) {
const deferredSearch = useDeferredValue(search);
// Lista re-filtra com deferred; input continua snappy
const filtered = useMemo(() =>
expensiveFilter(deferredSearch),
[deferredSearch]
);
return <List items={filtered} />;
}
Usuário digita rápido → search atualiza imediato (input responsive); filtered atualiza atrasado (sem trava).
onReset sem useQueryErrorResetBoundary: cache stale persiste; reset visual mas data não recarrega.error.tsx per route segment como segunda linha.componentDidCatch(error, info) ou onError em react-error-boundary → Sentry.captureException(error, { contexts: { react: info } }).Cruza com 03-09 §2.7 (hydration cost de streaming SSR), 03-07 §2.19 (error tracking em frontend), 02-04 §2.9.1 (Server Actions também precisam error boundary no client).
RSC é uma mudança fundamental no modelo. Componentes podem rodar só no servidor, não enviam JS pro cliente, podem await direto (assíncronos).
// app/orders/page.tsx (server component por default em Next.js App Router)
async function OrdersPage() {
const orders = await db.query('SELECT ...');
return <OrderList orders={orders} />;
}
Características:
useState, useEffect) nem refs nem eventos.Componentes client (com 'use client') coexistem. RSC pode importar e usar Client Components; o inverso só via children/props.
Modelo de "ilhas": página é majoritariamente HTML estático (RSC), com ilhas de interatividade (Client Components) onde precisa.
Implicação: muito do que se fazia com useEffect pra carregar data agora é await direto no server. getServerSideProps é history.
Server Actions (estável Next.js 14+, padrão em Next.js 16) são funções com 'use server' chamáveis do client como mutations. Substituem o ritual REST/tRPC para cenários CRUD do mesmo app.
Modelo mental:
'use server'.// app/orders/new/page.tsx (Server Component)
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { db } from '@/db';
import { auth } from '@/auth';
const CreateOrderSchema = z.object({
customer_email: z.string().email(),
items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1),
notes: z.string().max(500).optional(),
});
async function createOrder(prevState: unknown, formData: FormData) {
'use server'; // marker — NÃO vai pro client bundle
const user = await auth();
if (!user) return { error: 'unauthorized' };
const parsed = CreateOrderSchema.safeParse({
customer_email: formData.get('email'),
items: JSON.parse(formData.get('items') as string),
notes: formData.get('notes'),
});
if (!parsed.success) return { error: 'invalid', issues: parsed.error.flatten() };
// Tenant isolation — RLS via Postgres role/setting
const order = await db.transaction(async tx => {
await tx.execute(`SET LOCAL app.tenant_id = '${user.tenantId}'`);
return tx.insert(orders).values({ ...parsed.data, tenant_id: user.tenantId }).returning();
});
revalidatePath('/orders'); // RSC list re-renders sem manual mutation
return { ok: true, id: order[0].id };
}
export default function NewOrderPage() {
return <NewOrderForm action={createOrder} />; // Client Component imports server action
}
// components/new-order-form.tsx (Client Component)
'use client';
import { useActionState, useFormStatus } from 'react';
export function NewOrderForm({ action }: { action: any }) {
const [state, formAction] = useActionState(action, null);
const { pending } = useFormStatus();
return (
<form action={formAction}>
<input name="email" type="email" required />
<input name="items" type="hidden" defaultValue="[]" />
<textarea name="notes" />
<button disabled={pending}>{pending ? 'Criando…' : 'Criar pedido'}</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.ok && <p>Pedido {state.id} criado.</p>}
</form>
);
}
Optimistic UI com useOptimistic:
'use client';
import { useOptimistic } from 'react';
function OrderList({ orders, deleteAction }) {
const [optimistic, addOptimistic] = useOptimistic(orders, (state, id) =>
state.filter(o => o.id !== id)
);
return optimistic.map(o => (
<button key={o.id} onClick={async () => {
addOptimistic(o.id); // UI atualiza imediato
await deleteAction(o.id); // server action async
// se falhar, useOptimistic reverte; combine com toast de erro
}}>Cancelar pedido {o.id}</button>
));
}
Pegadinhas reais que mordem:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY). Sem isso, "Failed to find Server Action" em mismatched routes.<form action> ou useActionState pode bypass — sempre prefer abstractions oficiais.<input required> é UX, não security. Sempre Zod ou similar no server action.Promise<unknown> por default. Use ServerActionState<T> pattern com Zod schemas pra type-safe round-trip.Quando NÃO usar Server Actions:
Server Actions são pra mutations do same-app. Para tudo mais, fica em padrões anteriores.
use() hook e cache() em RSC patternsReact 19 (stable 2024) e React Compiler RC (2025) consolidam use() e cache() como primitivas centrais pra Server Components. use() lê promessas/contextos dentro de render conditional; cache() deduplica chamadas dentro de uma request. Sem entender, devs escrevem RSC com waterfall N+1, ou client components que poderiam ser server.
use() hook — o que é e o que NÃO é:
Promise<T> ou Context<T> e retorna o valor unwrapped.useState/useEffect). Essa é a feature crítica.use(promise) suspende até resolve, integra com <Suspense> pai.use(promise) também funciona, mas a promise tem que vir de server (passada como prop ou de cached source) — criar promise inline em client cria loop infinito.// app/order/[id]/page.tsx (Server Component)
import { use } from 'react';
import { db } from '@/lib/db';
export default function OrderPage({ params }: { params: { id: string } }) {
const order = use(db.orders.findById(params.id));
return (
<article>
<h1>Order #{order.id}</h1>
<Status status={order.status} />
</article>
);
}
Sem await. use() permite usar a promise como valor. Suspende automaticamente até resolve; <Suspense> boundary acima mostra fallback.
use() conditional — impossível com hooks tradicionais:
import { use } from 'react';
export function CourierBadge({
courierId,
loadDetails,
courierPromise,
}: {
courierId: string;
loadDetails: boolean;
courierPromise: Promise<Courier>;
}) {
if (!loadDetails) return <span>{courierId}</span>;
const courier = use(courierPromise); // OK: dentro do if
return <span>{courier.name} ({courier.rating})</span>;
}
Ganho: lazy-loading sem re-architecture. Componente decide se precisa do dado em render time. Constraint: courierPromise é estável (não recriada a cada render). Geralmente passada de server component pai.
cache() — request-scoped memoization:
getOrder(id) resultam em UMA query DB.unstable_cache ou revalidateTag.// lib/data/orders.ts
import { cache } from 'react';
import { db } from '@/lib/db';
export const getOrder = cache(async (id: string) => {
return db.orders.findById(id);
});
export const getCourier = cache(async (id: string) => {
return db.couriers.findById(id);
});
// app/order/[id]/page.tsx
import { getOrder, getCourier } from '@/lib/data/orders';
import { use } from 'react';
export default function Page({ params }: { params: { id: string } }) {
const order = use(getOrder(params.id));
return (
<>
<OrderSummary id={params.id} />
<AssignedCourier orderId={params.id} />
<h2>Order #{order.id}</h2>
</>
);
}
function OrderSummary({ id }: { id: string }) {
const order = use(getOrder(id)); // mesma cached promise
return <p>Total: {order.total}</p>;
}
function AssignedCourier({ orderId }: { orderId: string }) {
const order = use(getOrder(orderId)); // mesma cached promise; sem 2ª query
const courier = use(getCourier(order.courier_id));
return <p>Courier: {courier.name}</p>;
}
Sem cache(): 3 queries na DB (uma por componente). Com cache(): 1 query pra Order + 1 pra Courier.
Pegadinha — preload pattern pra evitar waterfall:
// Bad: waterfall (Page espera Order pra renderizar Items, Items espera Courier...)
const order = use(getOrder(id));
const items = use(getOrderItems(id)); // só inicia depois
const courier = use(getCourier(order.courier_id)); // depende do order
// Good: preload + parallel
getOrderItems(id); // fire-and-forget; promise cached
const order = use(getOrder(id));
const items = use(getOrderItems(id)); // promise já em flight; race resolves fast
Pattern oficial: chamar a função (não awaitar) early pra warm o cache; depois use() quando precisa. Documentado em https://react.dev/reference/react/cache (preload pattern).
Combinando use() + cache() + Suspense em Logística:
// app/dashboard/page.tsx
import { Suspense, use } from 'react';
import { getActiveOrders, getCourierStats, preloadCouriers } from '@/lib/data';
export default function Dashboard() {
// Preload pra warm cache; render imediato
preloadCouriers();
return (
<>
<h1>Operations dashboard</h1>
<Suspense fallback={<OrdersListSkeleton />}>
<ActiveOrders />
</Suspense>
<Suspense fallback={<CourierStatsSkeleton />}>
<CourierStats />
</Suspense>
</>
);
}
function ActiveOrders() {
const orders = use(getActiveOrders());
return <ul>{orders.map(o => <OrderRow key={o.id} order={o} />)}</ul>;
}
function CourierStats() {
const stats = use(getCourierStats());
return <StatsTable stats={stats} />;
}
Cada <Suspense> streama independente; user vê skeleton + primeiro card que resolve, depois o segundo.
Anti-patterns observados:
use() no client component com promise criada inline (use(fetch(url))): re-renderiza, recria promise, loop infinito. Use só com promise estável (passada de server, ou de cache()-d source, ou de React Query).cache() em client component: throws no build. Server-only.cache() esperando deduplicar entre requests: não, é por request. Pra cross-request use unstable_cache (Next) com tags + revalidateTag.use() espera sequencial, vira waterfall N+1. Preload no topo da árvore + cache resolve.await e use() no mesmo server component: funciona mas fica confuso. Padrão: server component com async usa await; nested server components usam use() de cached promises pra não criar waterfalls.cache() keying em objeto inline: cache(fn)({ id: x }) cria hash novo a cada render porque objeto é nova reference. Use args primitivos.use(Context) — bonus: use(MyContext) é equivalente a useContext(MyContext) mas pode ser conditional. Útil em árvores que entram/saem de provider.
Quando NÃO usar use() / cache():
cache() não faz sentido (sem request boundary). Use React Query / SWR.async/await em server component basta.Cruza com 02-04 §2.8 (Suspense é o partner natural de use()), 02-04 §2.9 (RSC fundamentos), 02-04 §2.9.1 (Server Actions são o complemento mutation-side), 04-09 §2.x (deduplication padrão também aplica em GraphQL DataLoader).
Estado local: useState/useReducer.
Estado de form: bibliotecas (React Hook Form, formik) ou primitivas + Zod schema.
Estado de servidor (queries, mutations): React Query (TanStack Query) ou SWR. Raramente Redux pra isso.
Estado global (UI): Context simples, ou Zustand, Jotai. Redux Toolkit pra times maiores.
URL state: searchParams. Em Next App Router é nativo.
Rule of thumb moderna: maior parte do "estado global" é estado de servidor. React Query resolve isso com cache, refetch, optimistic updates. Reduz drasticamente código de state global custom.
Ferramentas:
Patterns úteis:
react-virtual, tanstack/react-virtual) pra listas grandes.lazy() + Suspense.startTransition pra inputs que filtram listas pesadas.React.memo seletivo (pós-profiling).useCallback/useMemo).key correta em listas.Anti-patterns:
React.memo por todo lado sem medir.useCallback por todo lado.<Tabs><Tabs.List>...</Tabs.List></Tabs>). Usa Context interno.asChild, similar.React 19 stable (Maio 2025+, RC fim 2024) reescreve o modelo de formulário e introduz primitives de concurrent rendering production-ready. <form action={fn}> aceita função (server action OU client function) — não só URL string. Auto-pending state, auto-reset, integração com ErrorBoundary. Substitui ~80% dos use cases de form libraries; react-hook-form ainda relevante em forms complexas (field arrays, validação cross-field reativa).
useActionState (antes useFormState) — signature: const [state, dispatch, isPending] = useActionState(action, initialState);. Action recebe (prevState, formData) e retorna Promise<NewState>. Pattern Logística — criar pedido:
'use client';
import { useActionState } from 'react';
import { createOrderAction } from './actions';
export function CreateOrderForm() {
const [state, dispatch, isPending] = useActionState(createOrderAction, {
error: null, order: null,
});
return (
<form action={dispatch}>
<input name="pickup" required />
<input name="dropoff" required />
<button disabled={isPending}>{isPending ? 'Criando...' : 'Criar pedido'}</button>
{state.error && <p role="alert">{state.error}</p>}
{state.order && <p>Pedido #{state.order.id} criado</p>}
</form>
);
}
'use server';
export async function createOrderAction(prev: State, fd: FormData): Promise<State> {
const parsed = OrderSchema.safeParse(Object.fromEntries(fd));
if (!parsed.success) return { error: parsed.error.message, order: null };
try {
const order = await db.orders.insert(parsed.data);
revalidatePath('/orders');
return { error: null, order };
} catch (e) {
return { error: e.message, order: null };
}
}
useFormStatus — lê pending state do form ancestral. Substitui prop drilling de isPending. Pattern: <SubmitButton /> componente independente que sabe sozinho quando submit está em flight. Restrição: deve ser CHILD do <form>, não o form em si — usar no parent throws.
useOptimistic — optimistic UI antes do server response. Signature: const [optimistic, addOptimistic] = useOptimistic(serverState, reducer);. Pattern Logística — "marcar entregue":
export function OrderActions({ order }: { order: Order }) {
const [optimisticOrder, applyOptimistic] = useOptimistic(
order,
(curr, action: 'delivered') => ({ ...curr, status: action })
);
async function handleDelivered() {
applyOptimistic('delivered');
await markDeliveredAction(order.id); // se falhar, React reverte automaticamente
}
return (
<div>
<span>Status: {optimisticOrder.status}</span>
<form action={handleDelivered}><button>Marcar entregue</button></form>
</div>
);
}
Pegadinha crítica: useOptimistic reverte automaticamente quando action throws. Não precisa rollback manual; não usar try/catch pra reverter state.
useTransition (concurrent rendering) — const [isPending, startTransition] = useTransition(); marca update como non-urgent. URGENT updates (typing input, click feedback) NÃO são suspended; transition updates SIM. Pattern Logística — search filter list:
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
return (
<>
<input value={filter} onChange={e => {
setFilter(e.target.value); // urgent: input feedback instant
startTransition(() => {
router.push(`/orders?q=${e.target.value}`); // non-urgent: list re-render
});
}} />
{isPending && <Spinner />}
<OrdersList query={filter} />
</>
);
Concurrent rendering = React pode pause/resume/abandon work em transition. Tearing prevention em external stores (Zustand/Jotai/Redux) via useSyncExternalStore.
Suspense boundaries production — granularity: 1 boundary por unit de loading independente. Skeleton bem placed > spinner page-level. Pattern Logística dashboard:
<Dashboard>
<Suspense fallback={<MetricsSkeleton />}>
<Metrics tenantId={id} />
</Suspense>
<Suspense fallback={<OrdersListSkeleton />}>
<OrdersList tenantId={id} />
</Suspense>
</Dashboard>
Metrics e OrdersList carregam em paralelo, cada uma streams independente — Next.js 15+ App Router faz streaming HTTP por boundary.
Error boundaries integration — React 19 ErrorBoundary catches errors em renders, actions, sync code. Async errors de <form action> quando action throws: caught by parent ErrorBoundary OU retornados como state via useActionState. Pattern: ErrorBoundary fora do form (errors inesperados, network down); useActionState dentro (errors esperados, validation). Não confunde categorias.
useDeferredValue vs useTransition — useTransition envolve setState (você controla); useDeferredValue marca um VALUE como non-urgent (React decide). Use useDeferredValue quando consome value do parent que não controla: const deferredQuery = useDeferredValue(query);. Não combinar com useTransition na mesma value (redundante).
Logística applied stack:
useActionState + Server Action + Zod + revalidatePath.useOptimistic + Server Action; revert auto se erro.useTransition mantém input responsive enquanto re-render lista.Anti-patterns observados:
react-hook-form em form simples já resolvido por useActionState (overhead injustificado).useActionState com função sync (perde benefit; use direct setState).useOptimistic sem action async (hook é noop pra sync work).useTransition em todo setState (perde urgência de reactions; use só pra non-urgent).revalidatePath ou revalidateTag (UI fica stale após mutação).useActionState catch (UI quebra).useFormStatus no parent do form em vez de child (throws).useState para state que vem de URL (use searchParams + transition em search/filter).useDeferredValue + useTransition na mesma value (pick one).Cruza com: 02-05 (Next.js 15+, Server Actions integration), 02-08 (backend frameworks, server actions ≈ POST endpoints), 03-09 (frontend perf, transition prevents jank), 02-10 (ORMs Drizzle/Prisma em server actions), 02-19 (i18n, useActionState + locale validation).
React Compiler (RC) — what changes 2026: stable desde late 2024 / 2025; default-enabled em new Next.js 15+/Remix projects 2026. Compiler detecta quando components/values são pure e auto-wraps em memo() / useMemo() / useCallback() equivalents. Resultado: 90%+ das annotations manuais (useMemo, useCallback, React.memo) tornam-se desnecessárias. Migration: opt-in via Babel plugin; codemod oficial remove redundant memos.
Configuration (Next.js 15+ + React Compiler):
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
experimental: {
reactCompiler: true, // Enable React Compiler
},
};
export default config;
# Manual: babel-plugin-react-compiler
npm install babel-plugin-react-compiler
// .babelrc
{ "plugins": ["babel-plugin-react-compiler"] }
Mental model RC: tracks dependencies (sabe de que cada value depende; recompute apenas quando deps mudam); component memoization (auto-memoiza children quando props inalteradas); stable callbacks (function identity preservada cross-render quando closure values estáveis). Pattern Logística (before vs after RC):
// BEFORE — manual memo
const OrderRow = React.memo(({ order, onSelect }: Props) => {
const handleClick = useCallback(() => onSelect(order.id), [order.id, onSelect]);
const total = useMemo(() => calculateTotal(order.items), [order.items]);
return <div onClick={handleClick}>{total}</div>;
});
// AFTER — RC handles it
function OrderRow({ order, onSelect }: Props) {
const handleClick = () => onSelect(order.id);
const total = calculateTotal(order.items);
return <div onClick={handleClick}>{total}</div>;
}
O que RC NÃO faz (ainda manual): side effects (useEffect deps continuam responsabilidade sua); external state subscriptions (useSyncExternalStore inalterado); async operations (data fetching, mutations inalterados); component identity para refs/forwardRef (manual); edge cases violando Rules of React (ESLint plugin eslint-plugin-react-compiler flagga).
State management 2026 — landscape:
useState / useReducer (built-in): single component ou shallow tree; default.Zustand 5+ pattern Logística:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface OrdersStore {
orders: Order[];
selectedId: string | null;
selectOrder: (id: string) => void;
fetchOrders: () => Promise<void>;
}
export const useOrdersStore = create<OrdersStore>()(
devtools(
persist(
(set) => ({
orders: [],
selectedId: null,
selectOrder: (id) => set({ selectedId: id }),
fetchOrders: async () => {
const res = await fetch('/api/orders');
const orders = await res.json();
set({ orders });
},
}),
{ name: 'orders-store' } // localStorage key
)
)
);
// Component
function OrdersList() {
const orders = useOrdersStore((s) => s.orders); // selector
const select = useOrdersStore((s) => s.selectOrder);
return orders.map(o => <button onClick={() => select(o.id)}>{o.id}</button>);
}
Jotai 2.10+ pattern (atoms):
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
const ordersAtom = atom<Order[]>([]);
const selectedIdAtom = atomWithStorage<string | null>('selectedId', null);
// Derived atom (auto-computed from base atoms)
const selectedOrderAtom = atom((get) => {
const orders = get(ordersAtom);
const id = get(selectedIdAtom);
return orders.find(o => o.id === id);
});
function OrderDetail() {
const [selected] = useAtom(selectedOrderAtom); // re-renders only when selected changes
return selected ? <div>{selected.id}</div> : null;
}
Valtio 2+ pattern (proxy mutate-style):
import { proxy, useSnapshot } from 'valtio';
const state = proxy({
orders: [] as Order[],
selectedId: null as string | null,
});
// Mutate directly (proxy tracks)
function selectOrder(id: string) {
state.selectedId = id;
}
async function fetchOrders() {
state.orders = await (await fetch('/api/orders')).json();
}
function OrdersList() {
const snap = useSnapshot(state); // immutable snapshot; re-renders on access changes
return snap.orders.map(o => <button onClick={() => selectOrder(o.id)}>{o.id}</button>);
}
Decision matrix 2026:
| Lib | Best for | Bundle | API style | DevTools |
|---|---|---|---|---|
useState | Local component | 0 | Hooks | React DevTools |
| Context | Subtree config (theme, auth) | 0 | Hooks | React DevTools |
| Zustand | App-wide pragmático | ~3KB | Selector hooks | Redux DevTools middleware |
| Jotai | Granular atoms + derived | ~10KB | Hooks per atom | Jotai DevTools |
| Valtio | Mutate-style preference | ~5KB | Proxy | Valtio DevTools |
| RTK | Enterprise + time-travel | ~30KB | Slices + reducers | Redux DevTools full |
| TanStack Query | Server state | ~13KB | Hook per query | TQ DevTools |
Server state vs UI state — separation rígida: server state (orders fetched from API) → TanStack Query 5.60+ / SWR (auto-refetch + cache + invalidation). UI state (filter selected, modal open) → Zustand/Jotai/useState. Anti-pattern: armazenar server state em Redux/Zustand sem invalidation logic explícita (replica responsabilidade que TQ resolve gratuitamente).
Logística applied stack:
/orders, /couriers, /dashboard; auto-invalidate via mutation onSuccess + queryClient.invalidateQueries.useActionState (cobre §2.13) pra forms simples; React Hook Form pra wizards multi-step.next.config.ts (experimental.reactCompiler: true); codemod removeu 80% dos memos manuais; ESLint plugin enforce Rules of React.Anti-patterns observados (10 itens):
useMemo/useCallback everywhere (RC handle 90%; remove via codemod oficial).atomWithStorage ou Suspense boundary (sem persistence; flash on load).staleTime configurado em TQ; data outdated silenciosamente).Cruza com: 02-05 (Next.js + RC integration), 02-04 §2.7 (RC introduction inicial), 02-04 §2.13 (React 19 Forms + useActionState), 03-09 (frontend perf, RC reduz re-renders), 02-19 (i18n state via context vs atom).
use hook deep, View Transitions, ref as prop, Asset preloads, Document Metadata, useDeferredValue initialValueReact 19.0 GA saiu Q4 2024 estabilizando o que estava em RC desde 2023. 19.1 (Q1 2025) entregou owner stack tracking + overhaul de hydration error messages (diff visual SSR↔client + click-to-source). 19.2 (Q3 2025) introduziu <ViewTransition> component experimental (wrapper sobre View Transitions API). Next.js 15+ exige React 19; React Native 0.78+ suporta. View Transitions API: Chrome 111+ (Mar 2023), Edge 111+, Safari 18+ (Q4 2024), Firefox flagged em 2026. As features deste bloco mudam o trabalho diário — não são opcionais nem experimentais: são o baseline 2026.
use hook deep. Substitui o par useEffect+useState para promises e o useContext clássico. Diferenças cruciais: pode ser chamado condicionalmente (dentro de if, ternário, loop) — o único hook com essa permissão. Unwraps Promise (suspende o componente até resolve, integra com <Suspense> ancestral) ou Context (alternativa a useContext que funciona em fluxo condicional).
import { use, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// 1. Promise stable ref — criada FORA do render (server component, parent, ou cache)
async function fetchOrders(tenantId: string) {
const res = await fetch(`/api/tenants/${tenantId}/orders`, { cache: 'no-store' });
if (!res.ok) throw new Error(`Orders fetch failed: ${res.status}`);
return res.json() as Promise<Order[]>;
}
// Server Component cria a promise e passa pra Client Component
export default function OrdersPage({ tenantId }: { tenantId: string }) {
const ordersPromise = fetchOrders(tenantId); // executa no server, streamed
return (
<ErrorBoundary fallback={<OrdersError />}>
<Suspense fallback={<OrdersSkeleton />}>
<OrdersList ordersPromise={ordersPromise} />
</Suspense>
</ErrorBoundary>
);
}
'use client';
function OrdersList({ ordersPromise }: { ordersPromise: Promise<Order[]> }) {
const orders = use(ordersPromise); // suspende; resolve dispara render
return <ul>{orders.map(o => <OrderRow key={o.id} order={o} />)}</ul>;
}
// 2. Conditional use — IMPOSSÍVEL com useContext clássico
function ThemedRow({ order, useDarkContext }: { order: Order; useDarkContext: boolean }) {
const theme = useDarkContext ? use(DarkThemeContext) : use(LightThemeContext);
return <li style={{ color: theme.fg }}>{order.id}</li>;
}
View Transitions API integration. document.startViewTransition(() => stateUpdate()) snapshota o DOM antes/depois e anima o diff via CSS pseudo-elements (::view-transition-old, ::view-transition-new). Combinado com useTransition do React, transições de rota/lista ficam nativas sem libs (Framer Motion etc.) para casos simples. React 19.2 trouxe <ViewTransition> component experimental que declara o boundary e gera automaticamente view-transition-name único.
'use client';
import { useTransition, unstable_ViewTransition as ViewTransition } from 'react';
import { useRouter } from 'next/navigation';
function OrderRow({ order }: { order: Order }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const prefersReducedMotion = typeof window !== 'undefined'
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const navigate = () => {
const update = () => startTransition(() => router.push(`/orders/${order.id}`));
if (!document.startViewTransition || prefersReducedMotion) return update();
document.startViewTransition(update);
};
return (
<ViewTransition name={`order-${order.id}`}>
<li onClick={navigate} aria-busy={isPending}>{order.id} — {order.status}</li>
</ViewTransition>
);
}
CSS opcional: ::view-transition-old(order-*) { animation: fade-out 200ms; }.
ref as prop (no forwardRef). Em function components React 19, ref viaja como prop normal — forwardRef deprecated para componentes novos. Codemod oficial (npx codemod react/19/replace-act-import + react/19/ref-as-prop) migra automaticamente.
// ANTES (React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) =>
<input ref={ref} {...props} />
);
// DEPOIS (React 19) — ref é prop
function Input({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
Asset preloads (ReactDOM.preload / preinit / preconnect / prefetchDNS). Chamáveis dentro do render OU fora (módulo top-level). React deduplica por URL e injeta resource hints antes do paint. Substitui <link rel="preload"> manual.
import ReactDOM from 'react-dom';
function InvoiceViewer({ invoiceUrl }: { invoiceUrl: string }) {
ReactDOM.preload(invoiceUrl, { as: 'fetch', crossOrigin: 'anonymous' });
ReactDOM.preinit('https://cdn.example.com/pdf-viewer.js', { as: 'script' });
ReactDOM.preconnect('https://cdn.example.com');
ReactDOM.prefetchDNS('https://api.stripe.com');
return <PdfFrame url={invoiceUrl} />;
}
preinit baixa e executa; preload apenas baixa. Use preinit para scripts/CSS que rodarão em seguida; preload para fetch/font/image.
Document Metadata em render tree. <title>, <meta>, <link> declarados em qualquer profundidade são içados para <head> automaticamente. Aposenta react-helmet/next/head (em client components — Next.js app router tem metadata export para server side).
function OrderDetail({ order }: { order: Order }) {
return (
<>
<title>{`Pedido ${order.id} — Fathom Logística`}</title>
<meta name="description" content={`Status: ${order.status}, valor R$${order.total}`} />
<link rel="canonical" href={`https://app.fathom.com.br/orders/${order.id}`} />
<OrderBody order={order} />
</>
);
}
React não dedupe <title> — o último montado vence. Em apps com múltiplos componentes setando título, centralize.
useDeferredValue com initialValue. Argumento novo em 19.0: durante o primeiro render retorna initialValue (skeleton-friendly), depois adota o valor real. Elimina flash de "loading" em SSR + hydration de filtros pesados.
function OrdersFilter({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query, ''); // initialValue '' → render rápido
const results = useMemo(() => filterOrders(deferredQuery), [deferredQuery]);
return <ResultsList results={results} stale={query !== deferredQuery} />;
}
Hydration error UX (19.1). Mismatch agora mostra owner stack (cadeia de componentes que renderizou o nó), diff colorido entre HTML SSR e árvore client, link click-to-source via React DevTools. Reduz triagem de "Hydration failed" de horas para minutos.
Compiler ergonomics. Com React Compiler ativo (cf. §2.7, §2.14), idiomas antes proibidos passam: mutar array em event handler (items.push(x); setItems([...items]) deixa de ser anti-pattern em handlers — RC trata escopo). Não vale dentro de render — render permanece puro. useMemo/useCallback manuais ficam redundantes na maioria dos casos.
Stack Logística aplicada. Dashboard /dashboard/orders: Server Component cria ordersPromise = fetchOrders(tenantId), passa para <OrdersList> Client Component que faz use(ordersPromise) envolto em <Suspense fallback={<OrdersSkeleton />}>. Click em ordem dispara document.startViewTransition + router.push('/orders/:id') — fade entre lista e detalhe. Página de detalhe ReactDOM.preinit('/js/pdf-viewer.js') no topo + <title>Pedido #1234</title> + <meta> no JSX. Filtro de status usa useDeferredValue(query, '') para skeleton instant. Hydration warnings caem 80% após upgrade 19.1 graças a owner stack.
10 anti-patterns:
use(fetch('/api/x')) — promise criada inline em render → re-fetch loop infinito. Promise deve vir de server component, parent stable, ou cache (React.cache / SWR).document.startViewTransition sem checar prefers-reduced-motion — viola WCAG 2.3.3 (a11y). Sempre fallback para update direto.forwardRef em código novo após codemod — gera warning + impede inferência de tipo via ref prop.forwardRef interno e callers passando ref como prop — alguns componentes recebem, outros não. Padronize via codemod completo, não parcial.ReactDOM.preload(dynamicUrl) com URL gerada por Math.random()/timestamp — quebra dedup, gera N requests. Key estável por recurso.useDeferredValue(value) sem initialValue em SSR → primeira render usa value real (caro) → hydration mismatch quando client recalcula. Sempre forneça initialValue em SSR-rendered components.ReactDOM.preinit(url, { as: 'script' }) para script que não roda na página atual — desperdício de banda + execução; use preload se for opcional.<title> em múltiplos componentes irmãos sem coordenação — ordem de mount decide; resultado não-determinístico em re-renders.use(Context) em loop assumindo que substitui useReducer — use lê valor atual do Context, não cria estado. Para state local + Context, ainda precisa useReducer/useState.Cruza com: 02-04 §2.6 (hooks foundation), §2.7 (React Compiler), §2.8 (Suspense + streaming), §2.9 (RSC), §2.13 (React 19 Forms + Actions), §2.14 (RC + state mgmt 2026), 02-05 §2.23 (Next 15 async APIs + use compat), 03-09 (frontend perf — View Transitions são INP-friendly), 02-19 (i18n + Document Metadata por locale), 01-07 §2.12 (JS moderno 2026 — V8 Maglev/TurboShaft afetam React perf), 01-08 §2.16 (TS moderno 2026 — types pra components + RSC), 01-13 §2.17 (toolchain Rust — JSX transform via swc/oxc), 02-17 §2.20 (mobile native vs RN, KMP shared logic), 03-09 §2.21 (image optimization 2026), 03-17 §2.22 (a11y testing axe-playwright + Storybook).
Você precisa, sem consultar:
useEffect roda e por que Strict Mode dispara duas vezes.useEffect vs useLayoutEffect com case onde escolher cada.startTransition e quando ele ganha de simplesmente atualizar state.Construir um mini-React funcional o suficiente pra rodar uma TodoApp.
Implementar do zero, em TS:
createElement(type, props, ...children): equivalente a React.createElement. Retorna objeto { type, props, children }.tsconfig jsxFactory: 'createElement' ou pragma similar pra JSX virar suas chamadas.render(element, container), mount inicial.useState: list de hooks por componente, ordem importa.useEffect: roda após commit, com cleanup.useRef.onClick, onInput, etc. Adicionar/remover listeners corretamente em diff.requestIdleCallback + chunked work, à lá Fiber).Object.is é base de comparação shallow.Children tipos, polymorphic components.packages/react-reconciler/src/ReactFiberWorkLoop.js é o coração. Leia.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Por que React Fiber usa uma linked tree em vez de recursão pura para reconciliação?
Q2Em uma lista mutável com reorder, qual o problema de usar o índice como `key`?
Q3Qual a regra principal sobre Server Components e Client Components em RSC?
Q4Quando ainda vale escrever `useMemo` manualmente após adoção do React Compiler?
Q5Sobre `useOptimistic` em Server Actions: o que acontece se a action falhar?
Destrava
02-04 é prereq dos seguintes módulos: