JSX & Componentes
Function Components
Componentes React são funções JavaScript que retornam JSX. Nomes de componentes devem começar com letra maiúscula (minúscula = tags HTML). Props são passadas como atributos e desestruturadas no parâmetro. JSX é syntactic sugar para React.createElement(). Sempre retorne um único elemento raiz (ou use Fragments). Componentes devem ser puros — mesmas props = mesma saída.
// Basic function component
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
<p>Welcome, {name}!</p>
);
// Composing components
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
<Greeting />
</div>
);
}Expressões JSX
JSX permite incorporar expressões JavaScript em chaves {}. Você pode colocar variáveis, chamadas de função, operadores ternários e qualquer expressão que retorne um valor. Statements (if, for, switch) não são permitidos diretamente — use ternário ou IIFE. Valores booleanos (true, false), null e undefined renderizam como nada. Números e strings renderizam como texto. Objetos não são children React válidos.
function Expression({ user, items }) {
const fullName = user.first + ' ' + user.last;
const itemCount = items.length;
return (
<div>
{/* Expressions in curly braces */}
<h1>{fullName}</h1>
<p>{itemCount} items</p>
{/* Conditional */}
<p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>
{/* Method calls */}
<p>{fullName.toUpperCase()}</p>
{/* Numbers and booleans render as nothing */}
<p>{false}{null}{undefined}</p>
</div>
);
}Fragments & Listas em JSX
Fragments (<>...</>) agrupam múltiplos elementos sem adicionar nós DOM extras — mais limpo que envolver em um <div>. Use <Fragment key={...}> quando precisar passar uma key. Arrays de elementos JSX precisam de props key únicas. Embora o índice do array como key funcione para listas estáticas, use IDs estáveis para listas dinâmicas para evitar bugs de renderização. Fragments melhoram a performance reduzindo elementos wrapper desnecessários.
// Fragment: group without extra DOM node
function App() {
return (
<>
<header>Header</header>
<main>Content</main>
<footer>Footer</footer>
</>
);
}
// Array of elements (needs keys)
function List() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{fruits.map((fruit, i) => (
<li key={i}>{fruit}</li>
))}
</ul>
);
}Renderização Condicional
O React oferece múltiplos padrões de renderização condicional. Early returns para lógica if/else. Operador ternário (cond ? A : B) para um/ou outro. Logical AND (cond && <Component/>) para mostrar/ocultar. IIFE para ramificações complexas. Evite incorporar lógica complexa em JSX — extraia para variáveis ou funções auxiliares. Para statements switch, use um objeto de lookup ou extraia para uma função separada. Valores falsy (0, '') renderizam, então use ternário em vez de && para números.
function Greeting({ isLoggedIn, user }) {
// 1. If/else (use early return)
if (!isLoggedIn) return <Login />;
// 2. Ternary operator
return (
<div>
{user ? <Dashboard user={user} /> : <Loading />}
{/* 3. Logical AND (render if truthy) */}
{user.isAdmin && <AdminPanel />}
{/* 4. IIFE for complex logic */}
{(() => {
if (user.role === 'admin') return <Admin />;
if (user.role === 'mod') return <Mod />;
return <User />;
})()}
</div>
);
}Children & Render Props
A prop children contém elementos entre tags de abertura e fechamento — essencial para componentes compostáveis (cards, modais, layouts). Render props passam uma função como prop que recebe dados e retorna JSX — uma alternativa a HOCs e hooks para compartilhar lógica. Embora render props sejam menos comuns com hooks, ainda são úteis para padrões de injeção de componentes. children é uma prop especial que não precisa ser passada explicitamente.
// children prop: content between tags
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Profile">
<p>Name: Alice</p>
<p>Age: 30</p>
</Card>
// Render prop pattern
function DataProvider({ render }) {
const data = fetchData();
return <div>{render(data)}</div>;
}Props
Passando Props
Props são dados somente leitura passados do pai para o filho. Podem ser qualquer valor JavaScript: strings, números, booleanos, arrays, objetos ou funções. Valores string usam aspas (name='Alice'), todos os outros valores usam chaves (age={30}). Funções como props habilitam comunicação filho-para-pai (callbacks). Props fluem para baixo — filhos não podem modificar props. Para two-way data binding, eleve o state ao pai comum.
// Parent passes props to child
function App() {
return (
<User
name="Alice"
age={30}
isActive={true}
tags={['admin', 'dev']}
onClick={() => console.log('clicked')}
/>
);
}
// Child receives props
function User({ name, age, isActive, tags, onClick }) {
return (
<div onClick={onClick}>
<h1>{name}</h1>
<p>Age: {age}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}Props Padrão & Opcionais
Valores padrão de props são definidos via desestruturação (param = defaultValue). Se uma prop não é passada, é undefined. Use short-circuit (bio && <p>) ou ternário para renderizar condicionalmente props opcionais. PropTypes (legado) ou interfaces TypeScript podem validar tipos de prop em tempo de desenvolvimento. defaultProps (class components) está obsoleto para function components — use defaults de desestruturação.
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
return (
<button className={'btn btn-' + color + ' btn-' + size}>
{children}
</button>
);
}
// Optional props (undefined if not passed)
function Profile({ name, bio }) {
return (
<div>
<h1>{name}</h1>
{bio && <p>{bio}</p>}
</div>
);
}
// Usage
<Button>Click</Button> {/* color='blue', size='md' */}
<Profile name="Alice" /> {/* bio is undefined */}Spread & Rest Props
O operador spread (...props) passa todas as props a um elemento filho — útil para componentes wrapper (HOCs, styled components). O operador rest coleta as props restantes após desestruturar específicas. Esse padrão é comum em design systems onde um componente wrapper encaminha props desconhecidas a um elemento DOM. Cuidado: spreading pode sobrescrever atributos explícitos — a ordem importa ({...props} className='x' vs className='x' {...props}).
// Spread: pass all props to child
function Input(props) {
return <input {...props} className="input" />;
}
// Usage
<Input type="text" placeholder="Name" value="Alice" />
// Rest: collect remaining props
function Button({ label, ...rest }) {
return <button {...rest}>{label}</button>;
}
// Selective spreading
function Card({ title, children, ...divProps }) {
return (
<div {...divProps}>
<h2>{title}</h2>
{children}
</div>
);
}Prop Types & TypeScript
Interfaces TypeScript fornecem verificação de tipo em tempo de compilação para props — a abordagem recomendada para novos projetos React. Props opcionais usam ? (isActive?: boolean). PropTypes fornecem validação em runtime (apenas em desenvolvimento) e são úteis para projetos JavaScript sem TypeScript. isRequired garante que a prop seja fornecida. TypeScript captura erros de tipo antes do runtime, tornando-o superior para grandes codebases.
// TypeScript interface (recommended)
interface UserProps {
name: string;
age: number;
isActive?: boolean; // optional
onClick: (id: number) => void;
}
function User({ name, age, isActive = true, onClick }: UserProps) {
return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}
// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isActive: PropTypes.bool,
};Prop Drilling & Context
Prop drilling ocorre quando props passam por múltiplas camadas de componentes que não as usam. Para 2-3 níveis, é aceitável. Para árvores mais profundas, use Context API, bibliotecas de gerenciamento de state (Redux, Zustand) ou composição de componentes. Composição (passar componentes como props ou children) frequentemente resolve drilling de forma mais elegante que Context. Pergunte-se: todo componente intermediário precisa desses dados? Se não, reconsider sua estrutura de componentes.
// Prop drilling: passing through multiple levels
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserInfo user={user} />;
}
// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levelsuseState & State
useState Básico
useState é o hook fundamental para adicionar state a function components. Retorna um array: [currentValue, setterFunction]. O valor inicial pode ser de qualquer tipo. Chamar o setter dispara um re-render com o novo valor. Atualizações de state são assíncronas — o valor não muda imediatamente após chamar setCount. Cada instância de componente tem seu próprio state independente. O setter é estável (mesma referência entre renders).
import { useState } from 'react';
function Counter() {
// [currentValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}Atualizações Funcionais
Quando o novo state depende do state anterior, use uma atualização funcional: setCount(prev => prev + 1). Isso garante que você está trabalhando com o state mais recente, mesmo se múltiplas atualizações forem batched. Sem atualizações funcionais, chamadas sucessivas rápidas podem usar state obsoleto. O React 18 faz batch automático de atualizações de state (mesmo em promises e timeouts), então atualizações funcionais são essenciais para correção.
function Counter() {
const [count, setCount] = useState(0);
// BAD: may not work correctly with rapid updates
const incrementBad = () => setCount(count + 1);
// GOOD: functional update uses previous state
const incrementGood = () => setCount(prev => prev + 1);
// Batch updates
const addThree = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button onClick={addThree}>Count: {count}</button>;
}State com Objetos & Arrays
Nunca mute o state diretamente — sempre crie um novo objeto/array. Para objetos, use o operador spread para copiar propriedades existentes: {...prev, [field]: value}. Para arrays, use spread para adicionar ([...prev, newItem]), filter para remover e map para atualizar. O React compara referências para detectar mudanças — objetos mutados têm a mesma referência, então o React não re-renderiza. Esta é a fonte nº 1 de bugs React para iniciantes.
function Form() {
const [form, setForm] = useState({ name: '', email: '', age: 0 });
// BAD: mutates state directly
// form.name = 'Alice'; setForm(form);
// GOOD: spread to create new object
const updateField = (field, value) => {
setForm(prev => ({ ...prev, [field]: value }));
};
return (
<input
value={form.name}
onChange={e => updateField('name', e.target.value)}
/>
);
}
// Array state
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}State Inicial Preguiçoso
Se o state inicial requer uma computação cara, passe uma função para useState (lazy initialization). A função executa apenas no primeiro render, não em cada re-render. Isso é importante para analisar localStorage, buscar de IndexedDB ou qualquer setup intensivo de CPU. A forma de função: useState(() => initialValue). Para valores simples (números, strings), apenas passe o valor diretamente — lazy init é desnecessário.
import { useState } from 'react';
function ExpensiveInit() {
// BAD: runs on every render (even though result is ignored)
const [data, setData] = useState(computeExpensiveValue());
// GOOD: lazy initialization — function runs only once
const [data2, setData2] = useState(() => computeExpensiveValue());
// Reading from localStorage
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('user');
return saved ? JSON.parse(saved) : null;
});
return <div>{data2}</div>;
}Múltiplas Variáveis de State
Use múltiplas chamadas useState para valores independentes em vez de um grande objeto. Isso torna as atualizações mais simples (sem necessidade de spread) e evita re-renders desnecessários. Agrupe valores relacionados em um único objeto de state (ex.: campos de formulário). Para lógica de state complexa com múltiplos sub-valores, considere useReducer. Regra prática: se as atualizações de state são independentes, use useState separados; se são relacionadas/interdependentes, use useReducer ou um único objeto.
function LoginForm() {
// Multiple independent state variables
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
setIsSubmitting(true);
// ... validation and submission
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password}
onChange={e => setPassword(e.target.value)} />
<button disabled={isSubmitting}>Submit</button>
</form>
);
}useEffect & Side Effects
useEffect Básico
useEffect realiza side effects após o render. A função effect executa após o componente pintar. A função cleanup (retornada) executa antes do próximo effect e no unmount — essencial para limpar timers, subscriptions e listeners. O array de dependências controla quando o effect re-executa: [] = uma vez no mount, [dep] = quando dep muda, sem array = todo render. Sempre faça cleanup para evitar memory leaks.
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// Runs after every render
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function runs before next effect or unmount
return () => clearInterval(interval);
}, []); // empty array = run once on mount
return <p>Seconds: {seconds}</p>;
}Array de Dependências
O array de dependências é crítico para o comportamento do useEffect. Array vazio [] = apenas mount (como componentDidMount). Com dependências [a, b] = executa no mount e quando a ou b muda. Sem array = todo render (raramente o que você quer). Dependências ausentes causam stale closures. Incluir dependências desnecessárias causa re-execuções excessivas. Use a regra ESLint exhaustive-deps para capturar erros. Todo valor do escopo do componente usado no effect deve estar nas deps.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Runs once on mount (empty deps)
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs when userId changes
useEffect(() => {
fetch('/api/users/' + userId)
.then(r => r.json())
.then(setUser);
}, [userId]); // re-run when userId changes
// Runs on every render (no deps) - rarely needed
useEffect(() => {
console.log('Every render');
});
return <div>{user?.name}</div>;
}Cleanup & Subscriptions
Cleanup é essencial para subscriptions, event listeners, timers e conexões WebSocket. Sem cleanup, você obtém memory leaks e handlers duplicados. A função cleanup executa: (1) antes da re-execução do próximo effect, (2) no unmount do componente. Para WebSocket/event listeners, sempre remova-os no cleanup. Para state que depende do effect, resete-o no cleanup para evitar mostrar dados obsoletos de um roomId anterior.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com/' + roomId);
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// Cleanup: close connection when roomId changes or unmount
return () => {
ws.close();
setMessages([]); // reset for new room
};
}, [roomId]);
// Window event listener
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{messages.length} messages</div>;
}Buscando Dados
Buscar dados em useEffect requer uma flag de cancelamento para evitar definir state após unmount (causa avisos React). A flag 'cancelled' garante que setUsers/setError/setLoading só executem se o componente ainda estiver montado. Para apps de produção, considere usar uma biblioteca de data-fetching (React Query, SWR) que trata cache, deduplicação e race conditions automaticamente. O array de dependências vazio [] garante que a busca aconteça uma vez no mount.
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const res = await fetch('/api/users');
const data = await res.json();
if (!cancelled) setUsers(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
};
fetchData();
return () => { cancelled = true; };
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}useLayoutEffect vs useEffect
useEffect executa assincronamente após o navegador pintar — usuários podem ver um flash breve se você estiver medindo elementos DOM. useLayoutEffect executa sincronamente após mutações do DOM, mas antes da pintura — evitando flicker visual. Use useLayoutEffect para medições DOM (getBoundingClientRect, scroll position) que afetam o layout. Use useEffect para todo o resto (não bloqueia a pintura). No servidor, useLayoutEffect avisa — use o padrão useIsomorphicLayoutEffect.
import { useState, useEffect, useLayoutEffect } from 'react';
function Tooltip({ text }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// useEffect: runs AFTER paint (user may see flash)
useEffect(() => {
const rect = document.getElementById('tip').getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, [text]);
// useLayoutEffect: runs BEFORE paint (no flash)
useLayoutEffect(() => {
const rect = document.getElementById('tip').getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, [text]);
return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}useRef, useMemo & useCallback
Básico de useRef
useRef retorna um objeto mutável { current: value } que persiste entre renders. Diferente do state, alterar ref.current NÃO dispara um re-render. Usos comuns: (1) acessar elementos DOM (via atributo ref), (2) armazenar valores mutáveis que não afetam a renderização (timers, valores anteriores), (3) armazenar o valor mais recente para uso em callbacks. O objeto ref tem a mesma identidade entre renders. O valor inicial é passado para useRef(initialValue).
import { useRef } from 'react';
function FocusInput() {
// ref to access DOM element
const inputRef = useRef(null);
const focus = () => inputRef.current.focus();
const clear = () => {
inputRef.current.value = '';
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus</button>
<button onClick={clear}>Clear</button>
</div>
);
}useRef para Valores Mutáveis
useRef armazena valores mutáveis que persistem entre renders sem disparar re-renders. Isso é perfeito para: IDs de timer, referências WebSocket, rastrear state anterior e contar renders. Como alterar ref.current não causa um re-render, a UI não atualiza quando você o altera — use state para valores que devem afetar a UI. O padrão de contagem de render (ref.current++) é útil para debug, mas não deve ser usado em lógica de produção.
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const renderCount = useRef(0);
// Track render count (doesn't trigger re-render)
renderCount.current++;
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<div>
<p>{seconds}s (render #{renderCount.current})</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useMemo
useMemo memoiza (armazena em cache) um valor calculado, recomputando apenas quando as dependências mudam. Use para cálculos caros (filtragem, ordenação, matemática complexa) para evitar re-executar a cada render. O array de dependências funciona como useEffect. Uso excessivo pode prejudicar a performance (memoização tem sua própria sobrecarga) — memoize apenas operações genuinamente caras. useMemo também é útil para preservar referências de objeto para evitar re-renders filho.
import { useState, useMemo } from 'react';
function ProductList({ products, filter }) {
const [search, setSearch] = useState('');
// Memoize expensive computation
const filtered = useMemo(() => {
console.log('Filtering...');
return products
.filter(p => p.category === filter)
.filter(p => p.name.includes(search));
}, [products, filter, search]); // recompute only when these change
// Memoize a value
const totalPrice = useMemo(() =>
filtered.reduce((sum, p) => sum + p.price, 0),
[filtered]);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<p>Total: ${totalPrice}</p>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}useCallback
useCallback memoiza uma função, retornando a mesma referência entre renders, a menos que as dependências mudem. Isso evita re-renders desnecessários de componentes filho memoizados (envolvidos em memo()). Sem useCallback, todo render pai cria uma nova referência de função, causando re-renders dos filhos memo(). Use useCallback ao passar callbacks para componentes filho otimizados. Como useMemo, não o use em excesso — apenas para funções passadas como props para filhos memoizados.
import { useState, useCallback, memo } from 'react';
// Memoized child component
const Button = memo(function Button({ onClick, label }) {
console.log('Button rendered');
return <button onClick={onClick}>{label}</button>;
});
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Without useCallback: new function every render
// const handleClick = () => setCount(c => c + 1);
// With useCallback: stable function reference
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // empty deps = stable forever
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<Button onClick={handleClick} label="Click" />
<p>Count: {count}</p>
</div>
);
}Encaminhando Refs
forwardRef permite que componentes pai passem um ref a um elemento DOM de um componente filho. useImperativeHandle personaliza o que o ref expõe — em vez do nó DOM, você pode expor métodos específicos (focus, clear, getValue). Isso é útil para criar componentes de input reutilizáveis com APIs imperativas. O React 19 simplificou refs (ref agora é uma prop regular), mas forwardRef ainda é necessário para bibliotecas. Evite usar excessivamente imperative handles — prefira props declarativas.
import { useRef, forwardRef, useImperativeHandle } from 'react';
// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} />;
});
// Usage
function App() {
const ref = useRef(null);
return (
<>
<CustomInput ref={ref} />
<button onClick={() => ref.current.focus()}>Focus</button>
<button onClick={() => ref.current.clear()}>Clear</button>
</>
);
}useReducer & Context
Básico de useReducer
useReducer é uma alternativa ao useState para lógica de state complexa. Um reducer é uma função pura: (state, action) => newState. Actions descrevem o que aconteceu; o reducer decide como atualizar o state. Esse padrão torna as transições de state previsíveis e testáveis. Dispatch é estável (mesma referência). Sempre retorne um novo objeto de state (nunca mute). O caso default deve lançar um erro para actions desconhecidas. Use useReducer quando o state tem múltiplos sub-valores ou o próximo state depende de lógica complexa.
import { useReducer } from 'react';
// Reducer function: (state, action) => newState
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
case 'set':
return { count: action.payload };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
</div>
);
}Reducer Complexo
Reducers complexos gerenciam múltiplas partes relacionadas de state. Cada tipo de action trata uma transição de state específica. Sempre faça spread do state anterior ({...state}) para preservar campos não relacionados. Para atualizações aninhadas (como alternar um todo), use map para criar um novo array com o item atualizado. Reducers devem ser puros — sem side effects, sem chamadas de API. Extraia o reducer para um arquivo separado para testabilidade. Considere bibliotecas como Redux Toolkit para state muito complexo.
const initialState = {
todos: [],
filter: 'all',
loading: false,
};
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, todos: [...state.todos, action.todo] };
case 'toggle':
return {
...state,
todos: state.todos.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case 'delete':
return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
case 'set_filter':
return { ...state, filter: action.filter };
case 'set_loading':
return { ...state, loading: action.loading };
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
// dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}Context API
A Context API compartilha state através da árvore de componentes sem prop drilling. Crie com createContext(defaultValue). Envolva consumidores em Provider com uma prop value. Consuma com useContext(Context). Mudanças no valor do Context disparam re-renders de todos os consumidores. Para performance, divida contexts (ThemeContext, UserContext) para que componentes re-renderizem apenas quando seu context específico muda. O valor padrão é usado quando nenhum Provider envolve o consumidor.
import { createContext, useContext, useState } from 'react';
// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);
// 2. Provider component
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<Page />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// 3. Consume context
function Page() {
const { theme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}Context com Reducer
Combinar Context com useReducer cria um sistema leve de gerenciamento de state global (mini Redux). O Provider expõe tanto state quanto dispatch. Um hook personalizado (useStore) fornece tratamento de erros se usado fora do provider. Esse padrão é ótimo para apps de médio porte. Para apps muito grandes com atualizações frequentes, considere dividir contexts ou usar Redux/Zustand para evitar re-renderizar todos os consumidores a cada mudança de state.
import { createContext, useContext, useReducer } from 'react';
// Combine Context + useReducer for global state
const StoreContext = createContext(null);
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
// Custom hook for easy consumption
function useStore() {
const context = useContext(StoreContext);
if (!context) throw new Error('useStore must be used within StoreProvider');
return context;
}
// Usage
function Component() {
const { state, dispatch } = useStore();
return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}Performance do useContext
Quando o valor do context muda, TODOS os consumidores re-renderizam — mesmo se usam apenas uma pequena parte do valor. Para otimizar: (1) Divida contexts para que componentes se inscrevam apenas no que precisam. (2) Memoize o valor do context com useMemo para evitar re-renders quando o valor não mudou realmente. (3) Use selectors (biblioteca use-context-selector) para subscriptions de granularidade fina. Para atualizações de alta frequência (como posição do mouse), Context pode causar problemas de performance — considere refs ou external stores.
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();
// Each provider manages its own state
function App() {
return (
<ThemeProvider>
<UserProvider>
<CartProvider>
<App />
</CartProvider>
</UserProvider>
</ThemeProvider>
);
}
// Component only re-renders when its context changes
function ThemedButton() {
const { theme } = useContext(ThemeContext);
// Won't re-render when user or cart changes
return <button className={theme}>Button</button>;
}
// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}Eventos & Formulários
Tratamento de Eventos
Eventos React usam camelCase (onClick, não onclick). O objeto event é um SyntheticEvent (wrapper do evento nativo). e.preventDefault() para o comportamento padrão (envio de formulário, navegação de link). e.stopPropagation() impede o event bubbling. Para passar parâmetros a handlers, use arrow functions: onClick={() => handleDelete(id)}. Evite definir handlers complexos inline — extraia-os para legibilidade. Eventos React são pooled (pré-17), então chame e.persist() se precisar de acesso assíncrono.
function App() {
// Click event
const handleClick = (e) => {
e.preventDefault();
console.log('Button clicked', e.target);
};
// With parameters (use arrow function)
const handleDelete = (id) => {
console.log('Delete item', id);
};
return (
<div>
<button onClick={handleClick}>Click</button>
<button onClick={() => handleDelete(42)}>Delete</button>
<div onMouseEnter={() => console.log('hover')}
onMouseLeave={() => console.log('leave')}>
Hover me
</div>
</div>
);
}Inputs Controlados
Inputs controlados têm seu valor controlado pelo state React. A prop value define o valor do input, e onChange atualiza o state. Isso torna o React a 'única fonte de verdade' para dados de formulário. Cada tecla dispara uma atualização de state e re-render. Para formulários complexos, isso pode ser verboso — considere bibliotecas como React Hook Form ou Formik. Inputs controlados habilitam validação em tempo real e comportamento dinâmico. Sempre use onChange com value (ou readOnly) para evitar avisos React.
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Email:', email, 'Password:', password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}Formulário com Múltiplos Campos
Para formulários com muitos campos, use um único objeto de state e uma função handleChange genérica. O atributo name em cada input corresponde à chave do state. O handler usa nomes de propriedade computados ([name]: value) para atualizar o campo correto. Para checkboxes, use checked em vez de value. Esse padrão reduz boilerplate significativamente. Para inputs de arquivo, use inputs não controlados (não podem ser totalmente controlados). Considere React Hook Form para formulários complexos com validação.
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
country: 'us',
agree: false,
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input name="username" value={formData.username} onChange={handleChange} />
<input name="email" type="email" value={formData.email} onChange={handleChange} />
<select name="country" value={formData.country} onChange={handleChange}>
<option value="us">USA</option>
<option value="uk">UK</option>
</select>
<label>
<input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
Agree to terms
</label>
<button type="submit">Register</button>
</form>
);
}Inputs Não Controlados
Inputs não controlados usam refs para acessar o valor do DOM diretamente, sem state React. A prop defaultValue define o valor inicial (não value). Isso é mais simples para formulários que não precisam de validação em tempo real ou comportamento dinâmico. Inputs de arquivo devem ser não controlados (seu valor é somente leitura por segurança). Inputs não controlados também são úteis para integração com código não React. A desvantagem: você não pode validar ou transformar input facilmente em tempo real. Prefira inputs controlados para a maioria dos casos.
import { useRef } from 'react';
function UncontrolledForm() {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Email:', emailRef.current.value);
console.log('Password:', passwordRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={emailRef} type="email" defaultValue="" />
<input ref={passwordRef} type="password" defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}
// File input (must be uncontrolled)
function FileUpload() {
const fileRef = useRef(null);
return <input ref={fileRef} type="file" />;
}Validação & Tratamento de Erros
A validação de formulário pode ser feita no envio ou a cada mudança. A função validate retorna um objeto de erros — vazio significa válido. Exiba erros condicionalmente ao lado de cada campo. Para melhor UX, valide no blur (após o usuário sair do campo) em vez de a cada tecla. Bibliotecas como React Hook Form + Zod, ou Formik + Yup, fornecem schemas de validação robustos, gerenciamento de erros e rastreamento de touch/blur. Sempre valide no servidor também — validação client-side é para UX, não segurança.
function ValidatedForm() {
const [values, setValues] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const errs = {};
if (!values.email) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
if (!values.password) errs.password = 'Password is required';
else if (values.password.length < 8) errs.password = 'Min 8 characters';
return errs;
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate();
setErrors(errs);
if (Object.keys(errs).length === 0) {
console.log('Form valid', values);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={values.email}
onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
{errors.email && <span className="error">{errors.email}</span>}
<input type="password" value={values.password}
onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
{errors.password && <span className="error">{errors.password}</span>}
<button type="submit">Submit</button>
</form>
);
}Listas & Renderização Condicional
Renderizando Listas
Use .map() para transformar arrays em elementos JSX. Cada elemento precisa de uma prop key única — use IDs estáveis (todo.id), não índices de array. Keys ajudam o React a identificar quais itens mudam (adicionados, removidos, reordenados) para atualizações de DOM eficientes. Usar índice como key causa bugs quando itens da lista são reordenados ou inseridos no início. Para listas vazias, renderize uma mensagem de fallback. Considere useMemo para listas filtradas/ordenadas para evitar recomputação a cada render.
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => toggle(todo.id)}>
{todo.done ? 'Undo' : 'Done'}
</button>
</li>
))}
</ul>
);
}
// Filtering and sorting
function FilteredList({ items, filter }) {
const visible = items
.filter(item => item.category === filter)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{visible.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Keys Explicadas
Keys devem ser únicas entre irmãos (mesmo pai), mas podem se repetir entre listas diferentes. Keys ajudam o algoritmo de reconciliation do React: quando uma key muda, o React destrói e recria o componente (perdendo state). Com keys de índice, inserir um item no início desloca todos os índices, fazendo o React re-renderizar tudo. Com keys de ID estável, o React renderiza apenas o novo item. Keys não precisam ser globalmente únicas — apenas únicas dentro da lista. Não use keys aleatórias (Math.random()) — elas mudam a cada render.
// GOOD: stable, unique keys
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to
// Key must be unique among siblings
function List() {
return (
<div>
{users.map(u => <User key={u.id} user={u} />)}
{posts.map(p => <Post key={p.id} post={p} />)}
{/* IDs can repeat across different lists */}
</div>
);
}Padrões de Renderização Condicional
Existem múltiplos padrões de renderização condicional. Early returns são os mais limpos para guard clauses (loading, error, auth). Variáveis de elemento funcionam para if/else no meio do componente. Ternário (cond ? A : B) para um/ou outro em JSX. Logical AND (cond && <X/>) para mostrar/ocultar. Lookup de objeto para comportamento tipo switch. Evite ternários aninhados — extraia para variáveis ou componentes. Para números, use ternário em vez de && (0 && <X/> renderiza 0).
function UserDashboard({ user, loading, error }) {
// 1. Early returns for loading/error states
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <Login />;
// 2. Element variables
let greeting;
if (user.isAdmin) {
greeting = <h1>Welcome Admin {user.name}</h1>;
} else {
greeting = <h1>Welcome {user.name}</h1>;
}
return (
<div>
{greeting}
{/* 3. Ternary for either/or */}
{user.hasNotifications ? <NotificationBadge /> : null}
{/* 4. && for show/hide */}
{user.isAdmin && <AdminPanel />}
{/* 5. Switch via object lookup */}
{{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
[user.plan]}
</div>
);
}Filtragem & Busca em Listas
Listas pesquisáveis/filtráveis combinam useState para filtros com useMemo para performance. A função filter verifica tanto query quanto category. Sempre trate o estado vazio (sem resultados). Para listas grandes (1000+ itens), considere virtualização (react-window, react-virtualized) para renderizar apenas itens visíveis. Faça debounce do input de busca para chamadas de API. Busca case-insensitive usa toLowerCase(). Para filtragem complexa, extraia para uma função separada ou hook personalizado.
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
// Memoize filtered results
const filtered = useMemo(() => {
return items.filter(item => {
const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
const matchesCategory = category === 'all' || item.category === category;
return matchesQuery && matchesCategory;
});
}, [items, query, category]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">All</option>
<option value="food">Food</option>
<option value="tech">Tech</option>
</select>
{filtered.length === 0 ? (
<p>No results found</p>
) : (
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
</div>
);
}Componentes Dinâmicos
Renderização de componente dinâmico usa um objeto de lookup para mapear tipos a componentes. Isso é comum em conteúdo orientado por CMS, construtores de formulário e construtores de página. O mapa de componentes evita longas cadeias switch/if. Sempre trate tipos desconhecidos com um componente de fallback. Faça spread de props ({...block.props}) para passar todas as propriedades ao componente dinâmico. Esse padrão é flexível e extensível — adicionar um novo tipo de bloco apenas requer adicionar ao mapa. Capitalize a variável (Component) para que JSX a trate como componente.
// Render different components based on type
const componentMap = {
text: TextBlock,
image: ImageBlock,
video: VideoBlock,
quote: QuoteBlock,
};
function ContentRenderer({ blocks }) {
return (
<div>
{blocks.map(block => {
const Component = componentMap[block.type];
if (!Component) return <UnknownBlock key={block.id} type={block.type} />;
return <Component key={block.id} {...block.props} />;
})}
</div>
);
}
// Usage
const blocks = [
{ id: 1, type: 'text', props: { content: 'Hello' } },
{ id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];Otimização de Performance
React.memo
React.memo envolve um componente para evitar re-renders quando as props não mudaram (comparação shallow). Use para componentes que renderizam frequentemente com as mesmas props. O segundo argumento é uma função de comparação personalizada: retorne true para pular o re-render, false para re-renderizar. memo só ajuda se o componente for caro de renderizar ou for filho de um pai que renderiza frequentemente. Não envolva todo componente — memoização tem sobrecarga. Combine com useCallback/useMemo para efeito máximo.
import { memo } from 'react';
// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
console.log('Card rendered');
return (
<div className="card">
<h2>{title}</h2>
<p>{content}</p>
</div>
);
});
// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.value === nextProps.value;
});Code Splitting
Code splitting reduz o tamanho do bundle inicial carregando componentes sob demanda. React.lazy + Suspense habilita imports dinâmicos. A prop fallback mostra enquanto o componente carrega. Splitting baseado em rota (carregar componentes de página preguiçosamente) é o mais impactante. Splitting baseado em componente é útil para componentes pesados (charts, editores) que não são necessários imediatamente. Cada import lazy cria um chunk separado. Use React.lazy para default exports; para named exports, envolva em um módulo.
import { lazy, Suspense } from 'react';
// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Router>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/dashboard" element={<Dashboard />} />
</Router>
</Suspense>
);
}
// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
function Page({ showChart }) {
return (
<div>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}Virtualização para Listas Longas
Virtualização renderiza apenas os itens visíveis em uma lista longa, melhorando drasticamente a performance. react-window e react-virtualized são bibliotecas populares. Em vez de renderizar 10.000 nós DOM, apenas ~12 (visíveis) são renderizados, com um contêiner rolável. Isso reduz o tamanho do DOM e tempo de render. Use virtualização para listas com 100+ itens. A desvantagem: implementação mais complexa, problemas potenciais com search/find (itens não no DOM). Listas de altura variável precisam de VariableSizeList.
import { FixedSizeList } from 'react-window';
// Virtualized list: only renders visible items
function BigList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are renderedDebouncing & Throttling
Debouncing adia a execução até uma pausa na atividade (ex.: usuário para de digitar). Throttling limita a execução a uma vez por intervalo. Ambos evitam chamadas de API ou computações excessivas. O hook useDebounce atualiza o valor com debounce apenas após o usuário parar de digitar pelo atraso especificado. Isso é essencial para inputs de busca, handlers de resize e eventos de rolagem. Para throttling, use uma biblioteca como lodash.throttle ou implemente com timestamps. Sempre limpe timers em useEffect.
import { useState, useEffect } from 'react';
// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
// API call only fires when user stops typing for 300ms
useEffect(() => {
if (debouncedQuery) {
fetch('/api/search?q=' + debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}Profiling & Otimização
O componente Profiler mede tempos de render. phase é 'mount' ou 'update'. actualDuration é o tempo de render em milissegundos. Use React DevTools Profiler para flame charts visuais. Antes de otimizar, faça profile para encontrar gargalos reais — não adivinhe. Problemas comuns de performance: (1) re-renders desnecessários (corrija com memo/useMemo/useCallback), (2) cálculos caros (corrija com useMemo), (3) listas grandes (corrija com virtualização), (4) bundles grandes (corrija com code splitting). Otimização prematura desperdiça tempo — meça primeiro.
import { Profiler } from 'react';
function App() {
const onRender = (id, phase, actualDuration) => {
console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
};
return (
<Profiler id="App" onRender={onRender}>
<ExpensiveComponent />
</Profiler>
);
}
/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/Padrões & Error Boundaries
Custom Hooks
Custom hooks extraem lógica stateful reutilizável em uma função prefixada com 'use'. Eles podem chamar outros hooks. Custom hooks são a maneira principal de compartilhar lógica entre componentes (substituindo HOCs e render props). Retorne um objeto para múltiplos valores, ou um valor/array para valores únicos. Sempre trate estados de loading e error. O padrão de cancelamento (flag cancelled) evita atualizações de state após unmount. Nomeie hooks com prefixo 'use' para que as regras ESLint funcionem.
import { useState, useEffect } from 'react';
// Reusable data fetching hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(data => { if (!cancelled) { setData(data); setError(null); }})
.catch(err => { if (!cancelled) setError(err.message); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ id }) {
const { data: user, loading, error } = useFetch('/api/users/' + id);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <h1>{user.name}</h1>;
}Hook useLocalStorage
useLocalStorage persiste state em localStorage. O inicializador preguiçoso lê do localStorage no mount. O useEffect escreve no localStorage sempre que o valor muda. O try/catch trata erros de quota excedida e erros de parse JSON (dados corrompidos). Esse padrão funciona para qualquer state persistente: temas, preferências do usuário, conteúdo de rascunho. Para compatibilidade SSR, verifique typeof window !== 'undefined'. Para sync entre abas, escute o evento 'storage'. Hooks similares: useSessionStorage, useCookie.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('LocalStorage error:', e);
}
}, [key, value]);
return [value, setValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}Error Boundaries
Error boundaries capturam erros nos métodos render/lifecycle de componentes filho, evitando que todo o app trave. Devem ser class components (sem equivalente em hook ainda). getDerivedStateFromError atualiza o state para mostrar UI de fallback. componentDidCatch registra erros (envie para Sentry, LogRocket, etc.). Error boundaries NÃO capturam: event handlers, código assíncrono, setTimeout, erros no próprio boundary. Envolva seções específicas para isolar falhas. O botão 'Try again' reseta o state de erro.
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Caught error:', error, errorInfo);
// Send to error reporting service
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div>
<h1>Something went wrong.</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
);
}
return this.props.children;
}
}
// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>Higher-Order Components (HOC)
Higher-Order Components (HOCs) são funções que recebem um componente e retornam um aprimorado. Eram o padrão principal para compartilhar lógica antes dos hooks. Usos comuns: autenticação, estados de loading, theming. HOCs podem causar 'wrapper hell' (componentes profundamente aninhados) e colisões de props. Para novo código, prefira custom hooks — são mais simples, mais compostáveis e não adicionam à árvore de componentes. HOCs ainda são úteis para class components ou ao integrar com bibliotecas que os exigem.
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
return function WithLoading({ isLoading, ...props }) {
if (isLoading) return <div>Loading...</div>;
return <Component {...props} />;
};
}
// HOC for authentication
function withAuth(Component) {
return function WithAuth(props) {
const { user } = useContext(AuthContext);
if (!user) return <Redirect to="/login" />;
return <Component {...props} user={user} />;
};
}
// Usage
const UserList = withLoading(withAuth(BaseUserList));
// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibilityCompound Components
Compound components permitem que usuários componham um componente complexo a partir de partes simples. O pai (Select) fornece context, e componentes filho (Trigger, Options, Option) o consomem. Esse padrão é usado por bibliotecas como Radix UI, Headless UI e React Aria. Benefícios: API flexível (usuários podem reordenar/omitir partes), compartilhamento implícito de state via context, JSX limpo. Os componentes são anexados como propriedades estáticas (Select.Trigger). Esse é um padrão avançado — use para bibliotecas de UI reutilizáveis, não componentes únicos.
// Compound components: components that work together
function Select({ children, value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const context = { value, onChange, isOpen, setIsOpen };
return (
<SelectContext.Provider value={context}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
Select.Trigger = function Trigger({ children }) {
const { isOpen, setIsOpen } = useContext(SelectContext);
return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};
Select.Options = function Options({ children }) {
const { isOpen } = useContext(SelectContext);
return isOpen ? <div className="options">{children}</div> : null;
};
Select.Option = function Option({ value, children }) {
const { onChange, setIsOpen } = useContext(SelectContext);
return (
<div onClick={() => { onChange(value); setIsOpen(false); }}>
{children}
</div>
);
}
// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
<Select.Trigger>Choose...</Select.Trigger>
<Select.Options>
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select.Options>
</Select>Custom Hooks
Hook useFetch
Custom hooks extraem lógica stateful reutilizável em uma função prefixada com 'use'. useFetch encapsula busca de dados com estados loading/error. O AbortController cancela requisições em andamento quando o componente desmonta ou a URL muda (evitando race conditions e memory leaks). Sempre inclua cleanup em useEffect para operações assíncronas. Custom hooks podem chamar outros hooks (useState, useEffect, useContext). Eles são a maneira principal de compartilhar lógica entre componentes sem render props ou HOCs. Nomeie-os com prefixo 'use' para que o linter rules-of-hooks do React funcione.
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let abortController = new AbortController();
setLoading(true);
fetch(url, { ...options, signal: abortController.signal })
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => { setData(data); setError(null); })
.catch((err) => {
if (err.name !== "AbortError") setError(err.message);
})
.finally(() => setLoading(false));
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function Profile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{data.name}</div>;
}Hook useLocalStorage
useLocalStorage sincroniza state React com localStorage. O inicializador preguiçoso lê do localStorage apenas no primeiro render. O useEffect escreve no localStorage sempre que o valor muda. O try/catch trata casos onde localStorage está cheio ou desabilitado (navegação privada). Esse hook torna state persistente tão fácil quanto useState. Para sincronização entre abas, adicione um listener de evento storage. Para segurança SSR, proteja o acesso a window. Esse padrão funciona para sessionStorage também — apenas troque a API.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = window.localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error("LocalStorage write failed:", e);
}
}, [key, value]);
return [value, setValue];
}
// Usage: persists state across page reloads
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
return (
<div>
<button onClick={() => setTheme("dark")}>Dark</button>
<button onClick={() => setTheme("light")}>Light</button>
</div>
);
}Hook useDebounce
useDebounce adia a atualização de um valor até o usuário parar de digitar pelo atraso especificado. Isso é essencial para inputs de busca, autosave e chamadas de API acionadas por input do usuário — evita chamadas excessivas a cada tecla. A função cleanup limpa o timeout se o valor mudar novamente antes do atraso expirar. O valor com debounce só atualiza após a pausa, acionando efeitos downstream (como chamadas de API) com menos frequência. Para execução imediata com chamada trailing, use useThrottle. Combine com useFetch para busca eficiente enquanto digita.
function useDebounce(value, delay = 500) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage: debounce search input
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`)
.then((res) => res.json())
.then(setResults);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Hook usePrevious
usePrevious aproveita o fato de que useEffect executa após o render — ref.current ainda contém o valor antigo durante o render, então atualiza para o novo valor depois. Esse é um padrão comum para comparar state atual e anterior. useWindowSize rastreia dimensões do viewport com um listener de resize. Sempre limpe event listeners no return do useEffect para evitar memory leaks. Esses hooks utilitários demonstram como custom hooks encapsulam lógica relacionada ao DOM, tornando componentes mais limpos e a lógica reutilizável e testável.
function usePrevious(value) {
const ref = useRef(null);
useEffect(() => {
ref.current = value; // update AFTER render
}, [value]);
return ref.current; // returns previous value during render
}
// Usage: compare current vs previous
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Now: {count}, before: {prevCount}</p>
{count > prevCount && <p>Increased!</p>}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// useWindowSize hook
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handler = () =>
setSize({ width: innerWidth, height: innerHeight });
window.addEventListener("resize", handler);
return () => removeEventListener("resize", handler);
}, []);
return size;
}useToggle & useClipboard
useToggle simplifica state booleano com uma função toggle envolvida em useCallback para identidade estável. useClipboard envolve a clipboard API com um state de feedback 'copied' que auto-reset após um timeout. Esses pequenos hooks utilitários reduzem boilerplate e padronizam padrões comuns em todo seu app. O useCallback em ambos hooks evita re-renders desnecessários de filhos memoizados. Construir uma biblioteca de hooks pequenos e focados (useToggle, useClipboard, useMediaQuery, useOnClickOutside) acelera o desenvolvimento e garante comportamento consistente.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((p) => !p), []);
return [on, toggle, setOn];
}
function useClipboard(timeout = 2000) {
const [copied, setCopied] = useState(false);
const copy = useCallback((text) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), timeout);
});
}, [timeout]);
return [copied, copy];
}
// Usage
function CopyButton({ text }) {
const [copied, copy] = useClipboard();
return (
<button onClick={() => copy(text)}>
{copied ? "Copied!" : "Copy"}
</button>
);
}
function Modal({ children }) {
const [isOpen, toggle] = useToggle(false);
return (
<>
<button onClick={toggle}>Open</button>
{isOpen && <div className="modal">{children}</div>}
</>
);
}Portals
Criando um Portal
createPortal renderiza children em um nó DOM fora da hierarquia do componente atual (tipicamente document.body). Isso é essencial para modais, tooltips e dropdowns que devem escapar de restrições CSS do pai (overflow: hidden, stacking contexts de z-index, transform criando novos contexts). Apesar de renderizar em outro lugar no DOM, o event bubbling do React do portal ainda funciona como se estivesse na árvore original — handlers onClick em ancestrais ainda disparam. Isso dá o melhor dos dois mundos: escape visual de restrições do pai, mas fluxo de evento lógico preservado.
import { createPortal } from "react-dom";
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button onClick={onClose}>✕</button>
{children}
</div>
</div>,
document.body // render target outside the DOM hierarchy
);
}
// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
const [show, setShow] = useState(false);
return (
<div style={{ overflow: "hidden", position: "relative" }}>
<button onClick={() => setShow(true)}>Open Modal</button>
{show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
</div>
);
}Modal com Portal & Focus Trap
Um modal de produção precisa de mais que um portal: gerenciamento de foco (prender foco dentro, restaurar ao fechar), tratamento de tecla Escape, bloqueio de scroll do body e click-outside-to-close. Esta implementação salva o elemento previamente focado, foca o modal ao abrir e restaura o foco ao fechar — essencial para usuários de leitores de tela. Body overflow hidden impede rolagem do fundo. A função cleanup restaura tudo. Para prender foco completamente (ciclo de tab dentro do modal), use uma biblioteca como focus-trap-react. Sempre retorne null quando fechado para remover do DOM.
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
const modal = modalRef.current;
const previouslyFocused = document.activeElement;
modal.focus();
const handleKey = (e) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleKey);
// Prevent body scroll
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", handleKey);
document.body.style.overflow = "";
previouslyFocused.focus(); // restore focus
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div className="overlay" onClick={onClose}>
<div ref={modalRef} tabIndex={-1} className="modal">
{children}
</div>
</div>,
document.body
);
}Tooltips com Portals
Tooltips se beneficiam de portals porque devem overflow contêineres pai e evitar clipping. A posição do tooltip é calculada a partir do getBoundingClientRect() do gatilho e renderizada com position: fixed no nível do body. Isso evita problemas de z-index e overflow. Para posicionamento dinâmico (flip quando próximo à borda da tela), use uma biblioteca como Floating UI (anteriormente Popper.js). O portal garante que o tooltip nunca seja recortado por ancestrais overflow: hidden. Coordenadas de posicionamento fixo são relativas ao viewport, tornando o cálculo direto.
function Tooltip({ children, text }) {
const [visible, setVisible] = useState(false);
const [coords, setCoords] = useState({ x: 0, y: 0 });
const targetRef = useRef(null);
const show = () => {
const rect = targetRef.current.getBoundingClientRect();
setCoords({ x: rect.left, y: rect.top - 40 });
setVisible(true);
};
return (
<>
<span
ref={targetRef}
onMouseEnter={show}
onMouseLeave={() => setVisible(false)}
>
{children}
</span>
{visible && createPortal(
<div style={{ position: "fixed", left: coords.x, top: coords.y }}
className="tooltip">
{text}
</div>,
document.body
)}
</>
);
}
// Usage
<Tooltip text="Click to save">💾</Tooltip>Menus Dropdown com Portals
Menus dropdown enfrentam os mesmos problemas de overflow/z-index que tooltips. Portals resolvem o problema visual. O handler click-outside verifica se o alvo do clique está fora da ref do gatilho. O listener de scroll na fase capture (true como terceiro argumento) fecha o menu em qualquer rolagem, evitando que o menu se separe de seu gatilho. Para produção, use Floating UI que trata detecção de borda, flipping, shifting e atualizações automáticas de posicionamento em scroll/resize. Portals + lógica de posicionamento adequada = dropdowns robustos que funcionam em qualquer contexto de layout.
function Dropdown({ trigger, children }) {
const [open, setOpen] = useState(false);
const [pos, setPos] = useState({ top: 0, left: 0 });
const ref = useRef(null);
const handleOpen = () => {
const rect = ref.current.getBoundingClientRect();
setPos({ top: rect.bottom + 4, left: rect.left });
setOpen(true);
};
useEffect(() => {
if (!open) return;
const handleClick = (e) => {
if (!ref.current?.contains(e.target)) setOpen(false);
};
const handleScroll = () => setOpen(false); // close on scroll
document.addEventListener("mousedown", handleClick);
window.addEventListener("scroll", handleScroll, true);
return () => {
document.removeEventListener("mousedown", handleClick);
window.removeEventListener("scroll", handleScroll, true);
};
}, [open]);
return (
<>
<div ref={ref} onClick={handleOpen}>{trigger}</div>
{open && createPortal(
<div style={{ position: "fixed", ...pos }} className="dropdown">
{children}
</div>,
document.body
)}
</>
);
}Event Bubbling de Portal
Um recurso chave dos React Portals: event bubbling segue a árvore de componentes React, não a árvore DOM. onClick em um componente pai dispara mesmo quando o filho é portado para document.body. Isso significa que context, state e event delegation funcionam naturalmente. No entanto, herança CSS NÃO cruza a fronteira do portal — estilos no pai não cascateiam para conteúdo portado, pois estão em subárvores DOM diferentes. Você deve aplicar CSS explicitamente (via classes ou CSS variables em :root) para estilizar conteúdo do portal. Essa separação é geralmente desejável para modais/tooltips.
function PortalExample() {
// Despite rendering in document.body, events bubble
// through the React tree, not the DOM tree
return (
<div onClick={() => console.log("Parent clicked!")}>
<p>Click the button — parent handler fires!</p>
{createPortal(
<button onClick={() => console.log("Button clicked!")}>
I'm in a portal
</button>,
document.body
)}
</div>
);
}
// Clicking logs: "Button clicked!" then "Parent clicked!"
// This means context, state, and event delegation
// all work as if the portal were inline
// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply themSuspense & Lazy Loading
React.lazy & Suspense
React.lazy importa dinamicamente um componente, criando um bundle separado que carrega sob demanda (code splitting). Envolva componentes lazy em <Suspense> com um fallback (estado de loading) mostrado enquanto o chunk baixa. Isso reduz o tamanho do bundle inicial — usuários baixam apenas código para páginas que visitam. Cada chamada lazy() cria um chunk separado. Para splitting baseado em rota, carregue preguiçosamente cada componente de página. O fallback pode ser qualquer nó React (spinner, skeleton, texto). Suspense pode envolver múltiplos componentes lazy — o fallback mostra até todos estarem prontos.
import { lazy, Suspense } from "react";
// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<Suspense fallback={<div>Loading page...</div>}>
<nav>
<button onClick={() => setPage("dash")}>Dashboard</button>
<button onClick={() => setPage("settings")}>Settings</button>
</nav>
{page === "dash" && <Dashboard />}
{page === "settings" && <Settings />}
</Suspense>
);
}Suspense Aninhado
Limites Suspense aninhados criam um efeito de 'descascamento' onde o conteúdo é revelado progressivamente conforme cada chunk carrega. Suspense externo mostra seu fallback primeiro; conforme componentes internos carregam, eles se revelam independentemente. Isso evita que um único componente lento bloqueie toda a página. Coloque limites Suspense estrategicamente: ao redor de páginas em nível de rota (grosseiro), ao redor de seções principais (médio) e ao redor de widgets independentes (fino). Limites demais criam loading instável; de menos criam esperas longas. A chave é corresponder limites a unidades de conteúdo percebidas pelo usuário.
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<MainSkeleton />}>
<MainContent /> {/* loads first */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* loads independently, doesn't block MainContent */}
</Suspense>
</Suspense>
<Sidebar />
</Suspense>
// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visibleLazy com Error Boundaries
Lazy loading pode falhar (problemas de rede, deployments invalidando URLs de chunk). Error Boundaries capturam esses erros e mostram UI de fallback. Sempre envolva Suspense + lazy em um ErrorBoundary. O componentDidCatch registra erros para monitoramento. Para lógica de retry, você pode resetar o state do ErrorBoundary ou recarregar a página. Um padrão comum é um botão de retry que re-importa o chunk. Sem error boundaries, um carregamento de chunk falho trava todo o app. Isso é crítico para produção — confiabilidade de rede nunca é 100%.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error("Chunk load failed:", error, info);
}
render() {
if (this.state.hasError) {
return (
<div>
<p>Failed to load. {this.state.error.message}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
// Wrap lazy components: network failures need handling
<ErrorBoundary>
<Suspense fallback={<Loader />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>Busca de Dados com Suspense
O hook use() do React 19 habilita Suspense para busca de dados. Diferente do useEffect, use() suspende o componente até a promise resolver — o limite Suspense mais próximo mostra seu fallback. Múltiplas chamadas use() no mesmo componente resolvem concorrentemente (busca paralela). Isso elimina gerenciamento manual de estado de loading. A promise pode ser armazenada em cache fora do React para evitar refetch em re-render. Nota: use() só pode ser chamado em render ou dentro de hooks. Para React 18, use bibliotecas como React Query ou SWR que integram com Suspense.
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+
// Wrap a promise with use()
function UserProfile({ userId }) {
// 'use' suspends until the promise resolves
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
function fetchUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
// Parent provides Suspense boundary
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={1} />
</Suspense>
);
}
// Concurrent: multiple suspends resolve together
function Dashboard() {
const user = use(fetchUser(1));
const posts = use(fetchPosts(user.id));
// Both fetch in parallel, Suspense shows until all resolve
return <div>{user.name}: {posts.length} posts</div>;
}Suspense List (Orquestração)
SuspenseList orquestra a ordem de revelação de múltiplos limites Suspense. revealOrder='forwards' mostra itens em ordem (item 2 não se revela até item 1 estar pronto, mesmo se 2 carregar primeiro) — evita pulos de conteúdo. 'together' aguarda todos antes de revelar. 'backwards' revela de baixo para cima. tail='collapsed' oculta estados de loading para itens ainda não iniciados; 'hidden' oculta todos os fallbacks. Isso é útil para feeds e listas onde a ordem importa. Nota: SuspenseList era experimental e sua API pode mudar — verifique os docs atuais do React para disponibilidade.
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";
function Article({ id }) {
const data = use(fetchArticle(id));
return <article>{data.title}</article>;
}
function Feed() {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
{/* "forwards": reveals top-to-bottom as they load */}
{/* "together": waits for all, reveals together */}
{/* "backwards": reveals bottom-to-top */}
<Suspense fallback={<Skeleton />}>
<Article id={1} />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Article id={2} />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Article id={3} />
</Suspense>
</SuspenseList>
);
}React Router
Roteamento Básico
React Router v6 usa <BrowserRouter> como root, <Routes> para definir correspondência de rotas e <Route> com prop element (não component). <Link> cria links de navegação que usam a History API (sem recarga de página). Segmentos dinâmicos (:id) são acessados via useParams(). path='*' é um catch-all para 404s. Rotas são correspondidas por melhor correspondência, não ordem. Para parâmetros de busca de URL (?q=search), use useSearchParams(). BrowserRouter requer configuração do servidor para servir index.html para todas as rotas (fallback SPA).
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User 123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function UserPage() {
const { id } = useParams();
return <h1>User ID: {id}</h1>;
}Rotas Aninhadas & Outlet
Rotas aninhadas criam hierarquias de layout. O elemento da rota pai deve incluir <Outlet /> onde rotas filhas renderizam. A rota índice renderiza no path do pai. Rotas profundamente aninhadas (users/:id) criam layouts aninhados — layout Users envolve UserDetail. Isso é poderoso para dashboards com sidebars/headers persistentes. useOutlet() dá acesso ao elemento filho. A URL /users/123 renderiza Layout → Users → UserDetail, cada um contribuindo com seu layout. Isso substitui renderização condicional manual de layouts.
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users" element={<Users />}>
<Route path=":id" element={<UserDetail />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
}
function Layout() {
return (
<div>
<nav>Navigation here</nav>
<Outlet /> {/* Child routes render here */}
</div>
);
}
function Users() {
return (
<div>
<h2>Users</h2>
<Outlet /> {/* Nested :id route renders here */}
</div>
);
}Navegação & Redirecionamentos
useNavigate retorna uma função para navegação programática. navigate('/path', { replace: true }) substitui o histórico (sem botão voltar). Passe state para carregar dados para a próxima rota (ex.: para onde retornar após login). <Navigate> é o componente de redirecionamento declarativo — use-o em render para auth guards. NavLink fornece isActive para estilizar links ativos. useLocation dá a URL atual, pathname, search, hash e state. Para redirecionamentos após ações (envio de formulário), use navigate. Para redirecionamentos condicionais em render, use <Navigate>.
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";
function Login() {
const navigate = useNavigate();
const location = useLocation();
const handleLogin = async () => {
await auth.login();
// Redirect to intended page or home
const from = location.state?.from || "/";
navigate(from, { replace: true });
};
return <button onClick={handleLogin}>Login</button>;
}
// Declarative redirect
function ProtectedRoute({ user, children }) {
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
isActive ? "nav-active" : "nav"
}>
About
</NavLink>Loaders & Carregamento de Dados
React Router v6.4+ (data router) adiciona loaders (executam antes da rota renderizar) e actions (tratam envios de formulário). useLoaderData() acessa dados do loader — sem mais useEffect para buscar dados de rota. Loaders executam em paralelo para rotas aninhadas. errorElement captura erros de loaders/actions. Actions processam envios de formulário via <Form method='post'> — useActionData() retorna o resultado. Esse padrão (inspirado no Remix) coloca lógica de dados junto com rotas. As APIs de dados requerem createBrowserRouter/createHashRouter, não <BrowserRouter>.
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/users/:id",
element: <UserPage />,
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
if (!res.ok) throw new Response("Not found", { status: 404 });
return res.json();
},
errorElement: <ErrorPage />,
},
]);
function UserPage() {
const user = useLoaderData(); // data from loader
return <h1>{user.name}</h1>;
}
// Action for form submissions
{
path: "/users/new",
element: <NewUser />,
action: async ({ request }) => {
const formData = await request.formData();
const res = await fetch("/api/users", {
method: "POST",
body: formData,
});
return redirect(`/users/${res.id}`);
},
}
function App() {
return <RouterProvider router={router} />;
}Route Guards & Rotas Protegidas
Route guards protegem rotas com base em state de auth ou papéis. RequireAuth redireciona usuários não autenticados para login, preservando o destino pretendido em location.state para redirecionamento pós-login. RequireRole adiciona controle de acesso baseado em papel. Componha guards envolvendo-os (RequireAuth > RequireRole > component). Para rotas de layout, você também pode usar element={<RequireAuth><Outlet/></RequireAuth>} para proteger todas as rotas filhas de uma vez. Sempre verifique auth no servidor também — guards client-side são para UX, não segurança. O padrão escala para qualquer condição: subscription, feature flags, etc.
function RequireAuth({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={
<RequireAuth><Dashboard /></RequireAuth>
} />
<Route path="admin" element={
<RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
} />
</Route>
</Routes>
</BrowserRouter>
);
}
function RequireRole({ role, children }) {
const { user } = useAuth();
if (user?.role !== role) return <Navigate to="/forbidden" />;
return children;
}Gerenciamento de State (Context & Redux)
Padrão Context API
Context fornece state global sem prop drilling. Crie um context, envolva consumidores em um Provider e acesse via useContext. O hook personalizado useAuth adiciona verificação de erros e é a superfície de API recomendada. Context é ideal para atualizações de baixa frequência (auth, theme, locale). Para mudanças de state de alta frequência, Context faz todos os consumidores re-renderizarem a cada mudança — use useReducer para state complexo ou divida contexts. Sempre coloque o provider junto ao state que ele gerencia. O valor do Context deve ser memoizado com useMemo/useCallback se contiver funções.
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const user = await api.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be inside AuthProvider");
return ctx;
}
// Usage
function Navbar() {
const { user, logout } = useAuth();
return user
? <button onClick={logout}>Logout {user.name}</button>
: <Link to="/login">Login</Link>;
}
// Wrap app: <AuthProvider><App /></AuthProvider>useReducer + Context
useReducer + Context é o padrão recomendado para state global complexo sem bibliotecas externas. O reducer centraliza a lógica de state (transições previsíveis, testáveis). O provider memoiza o valor para evitar re-renders desnecessários. Valores derivados (total) são computados em useMemo. Esse padrão trata carrinho, state de formulário, wizards de múltiplas etapas, etc. Para apps verdadeiramente complexos com middleware, time-travel debugging ou muitas slices independentes, considere Redux Toolkit ou Zustand. Mas para a maioria dos apps, useReducer + Context é suficiente e tem zero dependências.
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case "ADD":
const existing = state.find((i) => i.id === action.item.id);
if (existing) {
return state.map((i) =>
i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
);
}
return [...state, { ...action.item, qty: 1 }];
case "REMOVE":
return state.filter((i) => i.id !== action.id);
case "CLEAR":
return [];
default:
return state;
}
}
function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const value = useMemo(() => ({
items,
total: items.reduce((s, i) => s + i.price * i.qty, 0),
addItem: (item) => dispatch({ type: "ADD", item }),
removeItem: (id) => dispatch({ type: "REMOVE", id }),
}), [items]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}Básico do Redux Toolkit
Redux Toolkit (RTK) é a maneira moderna e recomendada de usar Redux. createSlice auto-gera action creators e reducers. Usa Immer internamente, então você 'muta' state diretamente (state.value += 1) e Immer produz a atualização imutável. configureStore configura o store com padrões sensatos (Redux DevTools, thunk middleware). useSelector lê state; useDispatch despacha actions. RTK elimina boilerplate Redux (sem statements switch, sem constantes de tipo de action). Para lógica assíncrona, use createAsyncThunk. RTK Query (incluído) trata busca de dados e cache.
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; }, // Immer: mutate safely
decrement: (state) => { state.value -= 1; },
addBy: (state, action) => { state.value += action.payload; },
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
export const { increment, decrement, addBy } = counterSlice.actions;
// Usage in component
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(addBy(5))}>+5</button>
</div>
);
}Zustand (Alternativa Leve)
Zustand é uma biblioteca mínima de gerenciamento de state — sem providers, sem boilerplate. Crie um store com create(), acesse via hooks com funções selector. Selectors evitam re-renders: apenas componentes usando a slice alterada re-renderizam. Isso resolve o problema de re-render do Context sem a complexidade do Redux. Para selectors de objeto (retornando {a, b}), use comparação shallow para evitar re-renders desnecessários. Zustand suporta middleware (persist, devtools, immer). É ideal para apps pequenos a médios onde Redux é exagero, mas Context causa re-renders demais. A API é minúscula, mas poderosa.
import { create } from "zustand";
const useStore = create((set, get) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null }),
// Access other state with get()
doubleCount: () => get().count * 2,
}));
// Usage: select only what you need (prevents re-renders)
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
// Multiple selections
function Profile() {
const { user, setUser } = useStore(
(state) => ({ user: state.user, setUser: state.setUser })
);
// Use shallow comparison for object selectors
// import { shallow } from "zustand/shallow";
// useStore(selector, shallow);
}React Query (Server State)
React Query (TanStack Query) gerencia server state — dados buscados de APIs. Trata cache, refetch em segundo plano, dados obsoletos, atualizações otimistas e paginação automaticamente. queryKey identifica dados em cache (como uma chave de cache). staleTime controla por quanto tempo os dados são considerados frescos. invalidateQueries após mutações refaz queries dependentes. Diferente do Redux (client state), React Query é purpose-built para dados de servidor assíncronos. Elimina states manuais de loading/error, busca em useEffect e gerenciamento de cache. Para a maioria dos apps, React Query + state local (useState/useReducer) substitui Redux inteiramente.
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Users />
</QueryClientProvider>
);
}
function Users() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then((r) => r.json()),
staleTime: 60000, // fresh for 60s
refetchOnWindowFocus: true,
});
const mutation = useMutation({
mutationFn: (newUser) =>
fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
onSuccess: () => queryClient.invalidateQueries(["users"]),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.map((u) => <div key={u.id}>{u.name}</div>)}
<button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
</div>
);
}Testes (React Testing Library)
Teste Básico de Componente
React Testing Library (RTL) testa componentes como usuários interagem com eles — por role, label e texto, não detalhes de implementação. getByRole é a query preferida (testa acessibilidade também). userEvent simula interações reais de usuário (digitação, cliques) mais precisamente que fireEvent. Testes devem evitar testar state interno; em vez disso, verifique saída visível e comportamento. Se não conseguir consultar por role, use getByLabelText, getByText ou getByDisplayValue. Evite getByTestId a menos que necessário. Essa abordagem torna testes resilientes a refatoração — eles testam o que usuários veem e fazem.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";
test("counter increments on click", async () => {
const user = userEvent.setup();
render(<Counter />);
// Find by accessible role (not test-id)
expect(screen.getByRole("heading")).toHaveTextContent("0");
await user.click(screen.getByRole("button", { name: /increment/i }));
expect(screen.getByRole("heading")).toHaveTextContent("1");
});
test("displays error for invalid input", async () => {
const user = userEvent.setup();
render(<Form />);
await user.type(screen.getByLabelText(/email/i), "not-an-email");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});Testando Hooks
renderHook testa custom hooks em isolamento. result.current contém o valor de retorno do hook. Todas as atualizações de state devem ser envolvidas em act() para garantir que o React as processe sincronamente. rerender permite testar effects que dependem de props mutáveis. Para hooks assíncronos (useEffect com fetch), use waitFor ou queries findBy (que aguardam atualizações). Testar hooks diretamente é mais rápido e focado que testar através de um componente. No entanto, também teste hooks através de testes de integração de componente para verificar uso real. renderHook está disponível em @testing-library/react v13+.
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
test("useCounter increments and decrements", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
// Wrap state updates in act()
act(() => result.current.increment());
expect(result.current.count).toBe(1);
act(() => result.current.decrement());
expect(result.current.count).toBe(0);
act(() => result.current.reset());
expect(result.current.count).toBe(0);
});
// Testing with initial props that change
test("useEffect runs on dependency change", () => {
const { result, rerender } = renderHook(
({ id }) => useFetchUser(id),
{ initialProps: { id: 1 } }
);
rerender({ id: 2 });
// Effect re-ran with new id
});Testando Async & Mocking
MSW (Mock Service Worker) intercepta requisições de rede no nível do service worker — testes usam fetch() real, mas obtêm respostas mockadas. Isso é mais realista que mockar fetch diretamente. setupServer para Node (Jest), setupWorker para navegador. O lifecycle beforeAll/afterAll gerencia o servidor. server.use() sobrescreve handlers por teste. Queries findBy (assíncronas) aguardam elementos aparecerem — use para renderização assíncrona. queryBy retorna null se não encontrado (para afirmar ausência). waitFor faz polling por uma condição. MSW também pode ser usado para mocking de desenvolvimento e Storybook.
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";
// Mock API with MSW (Mock Service Worker)
const server = setupServer(
rest.get("/api/users/:id", (req, res, ctx) => {
return res(ctx.json({ id: 1, name: "Alice" }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("displays user after fetch", async () => {
render(<UserProfile id={1} />);
// findBy waits for async update
expect(await screen.findByText("Alice")).toBeInTheDocument();
expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});
test("shows error on fetch failure", async () => {
server.use(
rest.get("/api/users/:id", (req, res, ctx) =>
res(ctx.status(500))
)
);
render(<UserProfile id={1} />);
expect(await screen.findByText(/error/i)).toBeInTheDocument();
});Testando Context & Providers
Crie uma utilidade de render personalizada que envolve componentes com providers necessários (Theme, Auth, Router, etc.). Isso evita repetir setup de provider em todo teste. Re-exporte funções RTL do seu arquivo test-utils para que testes importem de lá. Para teste de router, use MemoryRouter (não BrowserRouter) com initialEntries para definir a URL inicial — sem histórico real de navegador necessário. Para Redux, envolva em um Provider de teste com um store real ou mock. Esse padrão mantém testes limpos e garante que todos os componentes tenham seu context necessário. É o setup padrão para qualquer infraestrutura de teste React.
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
function customRender(ui, { theme = "light", ...options } = {}) {
function Wrapper({ children }) {
return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
}
return render(ui, { wrapper: Wrapper, ...options });
}
// Re-export everything
export * from "@testing-library/react";
export { customRender as render };
// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";
test("button uses theme color", () => {
customRender(<Button>Click</Button>, { theme: "dark" });
expect(screen.getByRole("button")).toHaveClass("btn-dark");
});
// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
<MemoryRouter initialEntries={["/users/123"]}>
<App />
</MemoryRouter>
);Testando Eventos & Interações
userEvent.setup() cria uma instância de usuário para interações realistas: type (caractere por caractere), click, tab, keyboard (com códigos de tecla como {Escape}, {Enter}), selectOptions, upload e mais. Sempre aguarde interações do usuário — elas são assíncronas. Testar navegação por teclado é crucial para acessibilidade. Para teste de formulário, preencha todos os campos e verifique se onSubmit recebe dados corretos. userEvent é preferido a fireEvent porque simula comportamento real do navegador (focus, blur, eventos input na ordem correta). Teste o fluxo completo do usuário, não handlers de evento individuais.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("form submission flow", async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
// Fill form fields
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.type(screen.getByLabelText(/password/i), "password123");
// Submit
await user.click(screen.getByRole("button", { name: /login/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: "[email protected]",
password: "password123",
});
});
test("keyboard navigation", async () => {
const user = userEvent.setup();
render(<Modal />);
await user.tab(); // focus first element
expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
await user.keyboard("{Escape}"); // press Escape
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});TypeScript + React
Tipagem de Props de Componente
TypeScript com React fornece segurança de tipo para props. Use interfaces ou type aliases para props. Props opcionais usam ?. Union types (variant) limitam valores. React.ReactNode aceita qualquer conteúdo renderizável (strings, elementos, arrays). Estender HTML attributes (React.InputHTMLAttributes) permite que seu componente aceite todos os atributos nativos (placeholder, onChange, etc.) enquanto adiciona props personalizadas. O spread {...rest} passa atributos restantes ao elemento nativo. Esse padrão cria componentes flexíveis e type-safe. Sempre exporte tipos de prop para que consumidores possam referenciá-los.
// Basic props
interface ButtonProps {
text: string;
onClick: () => void;
variant?: "primary" | "secondary"; // union type
disabled?: boolean;
}
function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
}
// Children prop
interface CardProps {
title: string;
children: React.ReactNode; // any renderable content
}
// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
function Input({ label, error, ...rest }: InputProps) {
return (
<label>
{label}
<input {...rest} />
{error && <span className="error">{error}</span>}
</label>
);
}Hooks com TypeScript
TypeScript adiciona segurança de tipo a hooks. useState<T> especifica o tipo de state; useState<T | null>(null) para state anulável. useRef<T>(null) tipa a ref — current é T | null. Para useContext, defina um tipo de context e lance erro se undefined (para que consumidores obtenham o tipo não-undefined). Para useReducer, tipa a Action como uma discriminated union — o switch em action.type estreita o tipo em cada case, dando acesso type-safe ao payload. Esses padrões eliminam erros de runtime de acesso undefined e payloads de action incorretos.
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()
// useContext
interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
return ctx; // ctx is now ThemeContextType, not undefined
}
// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
switch (action.type) {
case "increment": return state + 1;
case "set": return action.value;
}
}, 0);Componentes Genéricos
Componentes e hooks genéricos funcionam com qualquer tipo de dados mantendo segurança de tipo. O parâmetro de tipo <T> é inferido da prop items, então renderItem e keyExtractor recebem automaticamente o tipo correto. É assim que TypeScript recria componentes utilitários genéricos (List, Table, Select) com segurança de tipo total. Hooks genéricos (useArray<T>) preservam tipos de forma similar através de operações. A insight chave: TypeScript infere T do uso, então consumidores raramente precisam especificá-lo explicitamente. Esse padrão é essencial para construir bibliotecas de componentes reutilizáveis e type-safe.
// Generic component: works with any type
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage: TypeScript infers T from items
<List
items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
// Generic hook
function useArray<T>(initial: T[]) {
const [array, setArray] = useState(initial);
const push = (item: T) => setArray((prev) => [...prev, item]);
return { array, push, setArray };
}Tipos de Evento & Refs
Tipos de evento React são específicos: ChangeEvent para inputs, FormEvent para formulários, MouseEvent para cliques. Cada é genérico sobre o tipo de elemento (e.target é corretamente tipado). forwardRef com TypeScript requer dois parâmetros de tipo: o tipo de ref e o tipo de props. forwardRef é necessário quando um componente precisa expor uma ref a um elemento DOM (para focus, medição, etc.). Sempre defina displayName para componentes forwardRef/memo para melhor debug no DevTools. React 19 permite ref como prop regular, reduzindo a necessidade de forwardRef, mas ainda é comum em código existente.
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // string
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.clientX, e.clientY);
}
// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => (
<label>
{label}
<input ref={ref} {...props} />
</label>
)
);
Input.displayName = "Input";
// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />Utility Types para Props
Utility types TypeScript são poderosos para composição de props. Pick seleciona props específicas (para componentes subset). Omit exclui props (para substituir comportamento). Partial torna todas as props opcionais (para padrões de prop padrão). ComponentProps<typeof Component> extrai o tipo de prop de um componente — útil para envolver/estender componentes existentes. Record<K, V> cria um tipo mapeando chaves a valores (ótimo para maps variante-para-classe). Essas utilidades habilitam definições de prop DRY e type-safe sem repetir interfaces. Domine-as para escrever código React + TypeScript manutenível.
// Pick: select specific props
interface ButtonProps {
text: string;
onClick: () => void;
color: string;
size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
icon: React.ReactNode;
};
// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
href: string;
};
// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;
// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
variant?: "custom";
};
// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;
// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;Concurrent Features (useTransition, useDeferredValue)
useTransition
useTransition marca uma atualização de state como não urgente (transition). Atualizações urgentes (valor do input) renderizam imediatamente para responsividade; atualizações não urgentes (filtrar 10.000 itens) podem ser interrompidas se o usuário digitar novamente. isPending indica que a transition está em andamento (mostre um indicador de loading sutil). Isso evita que a UI congele durante renders caros. A insight chave: o React pode interromper e descartar transitions obsoletas, mantendo a UI responsiva. Use para filtragem de busca, troca de abas e qualquer atualização de state que dispare renderização pesada. Não envolva atualizações urgentes (digitação, cliques) em transitions.
import { useTransition, useState } from "react";
function SearchResults() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleSearch = (value) => {
setQuery(value); // urgent: update input immediately
startTransition(() => {
// non-urgent: heavy filtering can be interrupted
const filtered = heavyFilter(allItems, value);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={(e) => handleSearch(e.target.value)} />
{isPending && <span>Updating...</span>}
<ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
</div>
);
}useDeferredValue
useDeferredValue é a contraparte declarativa ao useTransition. Retorna uma cópia adiada de um valor que atualiza com prioridade menor. O input atualiza imediatamente (urgente); a lista cara re-renderiza com o valor adiado (não urgente). React.memo no componente Results é crucial — evita re-renderizar a cada tecla, apenas quando deferredQuery muda. isStale (comparando atual vs adiado) permite mostrar um indicador visual (dimmed, spinner). Use useDeferredValue quando não controla a atualização de state (ex.: valor vem de props). Use useTransition quando controla a atualização.
function Search() {
const [query, setQuery] = useState("");
// query updates immediately; deferredQuery lags behind
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results query={deferredQuery} isStale={isStale} />
</div>
);
}
// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
const items = expensiveSearch(query); // heavy computation
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{items.map((i) => <div key={i.id}>{i.name}</div>)}
</div>
);
});useOptimistic (React 19)
useOptimistic (React 19) implementa atualizações otimistas — a UI atualiza imediatamente com o resultado esperado, então reconcilia com a resposta real do servidor. O state otimista mostra durante a operação assíncrona; quando os dados reais chegam (componente re-renderiza com novas props), o valor otimista é automaticamente substituído. Se a operação falhar, a atualização otimista simplesmente reverte no próximo render com props inalteradas. Isso elimina lógica manual de atualização otimista (rastrear state pendente, reverter em erro). Marque itens pendentes (pending: true) para mostrar indicadores de loading. Perfeito para likes, comentários e toggles.
import { useOptimistic } from "react";
function ThumbsUp({ likes, addLike }) {
// Optimistic state: updates immediately, reverts on error
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state, newLike) => [...state, newLike]
);
const handleClick = async () => {
const newLike = { id: Date.now(), pending: true };
addOptimisticLike(newLike); // instant UI update
try {
await addLike(newLike); // actual API call
} catch {
// Reverts automatically on re-render with real data
}
};
return (
<div>
<button onClick={handleClick}>👍 {optimisticLikes.length}</button>
{optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
</div>
);
}use (Hook do React 19)
use() é o novo hook do React 19 que lê context ou promises. Diferente do useContext, use() pode ser chamado condicionalmente (dentro de statements if, loops) — não tem a restrição rules-of-hooks. Para promises, use() suspende o componente até a promise resolver (requer um limite Suspense). A promise é criada no pai e passada como prop — isso inicia a busca durante o render (não em useEffect), permitindo que waterfalls comecem mais cedo. A mesma promise pode ser passada a múltiplos componentes (deduplicação). use() preenche a lacuna entre context síncrono e dados assíncronos.
import { use } from "react";
// Read context with use (works in conditions!)
function Theme() {
// Unlike useContext, use() can be inside conditions
if (showTheme) {
const theme = use(ThemeContext); // conditional context!
return <div style={{ background: theme.color }} />;
}
return null;
}
// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
// Suspends until promise resolves
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
// Parent passes promise (starts fetching during render)
function App() {
const userPromise = fetchUser(); // starts immediately
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}Padrões de Concurrent Rendering
Padrões concurrent: useTransition para trocas de aba não urgentes (mantém a nav responsiva enquanto conteúdo pesado renderiza). useSyncExternalStore inscreve com segurança em external stores (browser APIs, Redux, Zustand) em modo concurrent — fornece uma função snapshot para cliente e servidor (SSR-safe). Nunca use external mutable state diretamente em render (risco de tearing); sempre vá através de useSyncExternalStore. Os três argumentos: subscribe (retorna cleanup), getSnapshot (valor atual), getServerSnapshot (valor inicial SSR). Isso garante leituras consistentes durante renderização concurrent. Bibliotecas como Redux e Zustand usam isso internamente.
// Pattern 1: Deferred search with transition
function App() {
const [tab, setTab] = useState("home");
const [isPending, startTransition] = useTransition();
return (
<>
<nav>
<button
onClick={() => startTransition(() => setTab("analytics"))}
disabled={isPending}
>
{isPending ? "Loading..." : "Analytics"}
</button>
</nav>
{tab === "home" && <Home />}
{tab === "analytics" && <HeavyAnalytics />}
</>
);
}
// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
return useSyncExternalStore(
(callback) => {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
},
() => navigator.onLine, // client snapshot
() => true // server snapshot (SSR)
);
}Aprofundamento em Context API
createContext & Provider
createContext cria um objeto de context com um valor padrão usado quando nenhum Provider é encontrado. A prop value do Provider é consumida por todos os descendentes. Envolva o valor em useCallback/useMemo para evitar re-renders desnecessários.
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}useContext
useContext lê o valor do Provider mais próximo e re-renderiza o componente quando esse valor muda. Aninhe múltiplos Providers para diferentes preocupações. Divida contexts por frequência de atualização para performance.
function ThemedButton() {
const { theme, toggle } = useContext(ThemeContext);
return (
<button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
Toggle Theme
</button>
);
}Context com Reducer
Combinar useReducer com Context cria um store global sem Redux. O reducer centraliza a lógica de state; o Context distribui state e dispatch. Consumidores podem despachar actions sem prop-drilling.
const StoreContext = React.createContext(null);
function storeReducer(state, action) {
switch (action.type) {
case 'add': return { items: [...state.items, action.item] };
case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
default: return state;
}
}
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(storeReducer, { items: [] });
return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}Otimizando Renders de Context
Quando state e dispatch vivem no mesmo context, toda mudança de state re-renderiza todos os consumidores. Dividi-los significa que componentes apenas de dispatch nunca re-renderizam em mudanças de state. dispatch do useReducer é estável.
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}Custom Hook para Context
Envolver useContext em um hook personalizado dá uma API limpa e um erro claro quando o Provider está ausente. Exporte tanto o Provider quanto o hook. Esta é a maneira recomendada de consumir context.
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}useReducer
useReducer Básico
useReducer é uma alternativa ao useState para lógica de state complexa. O reducer é uma função pura: (state, action) => newState. dispatch é estável, então você pode passá-lo sem se preocupar com re-renders.
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
default: throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}Inicialização Preguiçosa
O terceiro argumento para useReducer é uma função init que executa uma vez durante o render inicial. Isso é útil quando o state inicial é caro de computar ou quando você quer que reset retorne a um state computado.
function init(initialCount) {
return { count: initialCount, history: [] };
}
function reducer(state, action) {
switch (action.type) {
case 'increment': return { ...state, count: state.count + 1 };
case 'reset': return init(action.payload);
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, initialCount, init);Shape de State Complexo
useReducer brilha quando o state tem múltiplos campos relacionados. Cada action descreve uma transição completa de state, tornando a lógica mais fácil de rastrear que chamadas setState espalhadas. Mantenha o reducer puro.
const initialState = { users: [], loading: false, error: null, filter: 'all' };
function reducer(state, action) {
switch (action.type) {
case 'fetch-start': return { ...state, loading: true, error: null };
case 'fetch-success': return { ...state, loading: false, users: action.users };
case 'fetch-error': return { ...state, loading: false, error: action.error };
case 'set-filter': return { ...state, filter: action.filter };
default: return state;
}
}Reducer com Context
Combinar useReducer com Context cria um store leve tipo Redux. O reducer contém a lógica; o Context distribui state e dispatch. Este é o padrão recomendado para state em nível de app em apps médios.
function todoReducer(state, action) {
switch (action.type) {
case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
case 'delete': return state.filter((t) => t.id !== action.id);
default: return state;
}
}
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}Action Types & Padrões
Defina action types como constantes para evitar typos e habilitar autocomplete da IDE. O shape da action { type, payload? } é uma convenção comum. Para TypeScript, defina uma discriminated union de action types.
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };
function reducer(state, action) {
switch (action.type) {
case ACTIONS.ADD:
return [...state, { id: action.id, ...action.payload }];
case ACTIONS.UPDATE:
return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
case ACTIONS.DELETE:
return state.filter((item) => item.id !== action.id);
default: return state;
}
}useMemo & useCallback
useMemo
useMemo armazena em cache o resultado de uma computação e apenas recomputa quando as dependências mudam. Use para cálculos caros (ordenação, filtragem de arrays grandes). O array de dependências deve incluir tudo que o callback usa.
function ProductList({ products, filter }) {
const filtered = useMemo(() => {
return products.filter((p) => p.category === filter);
}, [products, filter]);
const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}useCallback
useCallback memoiza uma função para que mantenha a mesma identidade entre renders, a menos que as dependências mudem. Isso é crítico ao passar callbacks a filhos memoizados — sem isso, o filho re-renderiza toda vez.
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount((c) => c + 1), []);
return <MemoizedChild onClick={handleClick} />;
}React.memo
React.memo envolve um componente para que apenas re-renderize quando suas props mudarem (comparação shallow). O segundo argumento é um comparator personalizado retornando true para pular o re-render. Combine memo com useCallback/useMemo para props.
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
return <li onClick={onClick}>{value}</li>;
});
// With custom comparison
const DeepChild = React.memo(
({ user }) => <div>{user.name}</div>,
(prev, next) => prev.user.id === next.user.id
);Quando Memoizar
Memoização tem um custo que pode exceder as economias. Apenas memoize quando: (1) a computação é cara, (2) o valor é passado a um filho memoizado, ou (3) o valor é usado como dependência em useEffect/useMemo.
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);
// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);
// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);useMemo para Igualdade Referencial
useMemo garante que objetos e arrays mantenham a mesma referência entre renders, o que importa quando são usados como dependências em useEffect/useMemo/useCallback. Sem isso, { q: query } cria um novo objeto a cada render.
function Search({ query }) {
const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
useEffect(() => {
api.search(params).then(setData);
}, [params]); // Without useMemo, this fires every render
}Portals & Refs
createPortal
createPortal renderiza children em um nó DOM fora da árvore DOM do componente pai (geralmente document.body). Isso é essencial para modais, tooltips e dropdowns que devem escapar de stacking contexts do pai.
import { createPortal } from 'react-dom';
function Modal({ open, onClose, children }) {
if (!open) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
</div>,
document.body
);
}useRef
useRef retorna um objeto mutável cujo .current persiste entre renders sem causar re-renders. O uso principal é acessar nós DOM. Também armazena valores mutáveis que não afetam a UI.
function FocusInput() {
const inputRef = useRef(null);
const focus = () => inputRef.current?.focus();
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus</button>
</>
);
}forwardRef
forwardRef permite que um componente pai passe uma ref através de um componente wrapper a um nó DOM filho. Sem isso, o React proíbe passar ref como prop. A ref é o segundo argumento para a função envolvida.
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
return (
<label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
);
});useImperativeHandle
useImperativeHandle personaliza a instância exposta ao pai via ref — em vez do nó DOM bruto, o pai vê apenas os métodos que você define. Use com moderação; prefira props declarativas quando possível.
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
}));
return <video ref={videoRef} src={props.src} />;
});Refs para Valores Mutáveis
useRef armazena valores mutáveis que não devem disparar re-renders — como IDs de timer, instâncias WebSocket ou flags 'is mounted'. Sempre limpe side effects na função cleanup do useEffect.
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
};
const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
useEffect(() => () => clearInterval(intervalRef.current), []);
return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}Error Boundaries
Error Boundary de Classe
Error boundaries são class components que capturam erros em sua árvore de componentes filho durante a renderização. getDerivedStateFromError atualiza state para renderizar o fallback; componentDidCatch registra o erro. Eles NÃO capturam erros em event handlers ou código assíncrono.
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(error) { return { hasError: true, error }; }
componentDidCatch(error, info) { console.error('Caught:', error, info); }
render() {
if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
return this.props.children;
}
}Usando Error Boundaries
Coloque error boundaries estrategicamente para que uma falha em uma parte da UI não derrube todo o app. Limites granulares ao redor de widgets permitem que o resto do app continue funcionando.
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Header />
<ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
<ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
</ErrorBoundary>
);
}Resetando State de Erro
Error boundaries permanecem no state de erro até que seu state mude. Forneça um botão 'Try again' que reseta hasError para false, permitindo que o React re-renderize os filhos. Mudar a prop key também o reseta.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) { return { hasError: true, error }; }
reset = () => this.setState({ hasError: false, error: null });
render() {
if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
return this.props.children;
}
}Biblioteca react-error-boundary
A biblioteca react-error-boundary fornece um error boundary polido e amigável a hooks sem escrever uma classe. FallbackComponent recebe o error e uma função resetErrorBoundary. resetKeys auto-reseta quando esses valores mudam.
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logError(error, info)}
onReset={() => window.location.reload()}
resetKeys={[location.pathname]}
>
<Routes />
</ErrorBoundary>Tratamento de Erros Assíncronos
Error boundaries não capturam erros em promises, setTimeout ou event handlers. Para levar erros assíncronos a um boundary, armazene o erro em state e lance-o novamente durante o render. O boundary então o captura.
function AsyncComponent() {
const [state, setState] = useState({ data: null, error: null });
useEffect(() => {
let active = true;
fetchData()
.then((data) => { if (active) setState({ data, error: null }); })
.catch((error) => { if (active) setState({ data: null, error }); });
return () => { active = false; };
}, []);
if (state.error) throw state.error;
if (!state.data) return <Loading />;
return <div>{state.data}</div>;
}Otimização de Performance
Virtualização
Virtualização renderiza apenas as linhas visíveis de uma lista longa, reduzindo drasticamente os nós DOM. Essencial para listas com mais de 1000 itens — sem isso, o navegador engasga com dezenas de milhares de nós.
import { useVirtualizer } from '@tanstack/react-virtual';
function BigList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((vi) => (
<div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
{items[vi.index].name}
</div>
))}
</div>
</div>
);
}Code Splitting
Code splitting quebra o bundle em chunks carregados sob demanda. Os splits mais impactantes são em nível de rota (cada página é um chunk separado) e widgets pesados (bibliotecas de charts, editores). Meça com bundle analyzers.
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));
function Page({ showChart }) {
return (
<Suspense fallback={<Skeleton />}>
{showChart && <Chart data={data} />}
</Suspense>
);
}Profiling com DevTools
O React DevTools Profiler registra tempos de render e mostra quais componentes re-renderizaram e por quê. Procure por renders 'desperdiçados' onde as props não mudaram realmente — esses são candidatos para React.memo.
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)
// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
console.time('render');
const result = heavyCompute(data);
console.timeEnd('render');
return <div>{result}</div>;
}useDeferredValue
useDeferredValue adia a atualização de um valor, deixando atualizações urgentes (digitação) acontecerem primeiro. O render caro usa o valor adiado, então não bloqueia o input. isStale permite mostrar uma dica visual sutil.
function Search({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}Concurrent Features
As concurrent features do React 18 mantêm a UI responsiva durante trabalho pesado. useTransition e useDeferredValue permitem que o React interrompa renders para tratar input urgente. O batching automático agrupa múltiplas chamadas setState em um re-render.
import { useTransition, useDeferredValue } from 'react';
// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));
// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
setLoading(false); // \
setData(data); // > single re-render
setError(null); // /
});Testes (React Testing Library)
Render & Query Básico
render() monta um componente em um DOM falso; screen o consulta. Prefira queries por role (getByRole) — elas espelham como a tecnologia assistiva vê a página e impõem acessibilidade. userEvent simula interações reais de usuário.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('increments on click', async () => {
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});Variantes de Query
getBy afirma que o elemento existe (lança exceção caso contrário). queryBy é para afirmar ausência (retorna null). findBy aguarda elementos assíncronos aparecerem. As variantes All tratam múltiplas correspondências.
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });
// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();
// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');
// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');Disparando Eventos
userEvent (não o fireEvent de nível inferior) é a maneira recomendada de simular interações — ele dispara todos os eventos que um usuário real dispararia (focus, input, keydown, click) na ordem correta. Sempre use await com métodos userEvent.
import userEvent from '@testing-library/user-event';
test('form submission', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<Form onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalled();
});waitFor & Async
waitFor faz polling até a asserção passar ou atingir timeout. findBy* combina waitFor e getBy para o caso comum de 'aguardar isto aparecer'. within limita queries a um elemento específico.
import { waitFor, within } from '@testing-library/react';
test('shows data after fetch', async () => {
render(<UserList />);
await waitFor(() => {
const list = screen.getByRole('list');
expect(within(list).getAllByRole('listitem')).toHaveLength(3);
});
});
// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
render(<UserList />);
expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});Mocking & Setup
MSW (Mock Service Worker) intercepta requisições de rede no nível do service worker, então seu código fetch executa sem modificações. Configure handlers por teste, resete entre testes e feche após todos.
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('shows user name', async () => {
render(<UserProfile />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});Snippets de React relacionados
Copy-paste ready code for common tasks.
Formulário Controlado com Validação
Construir um formulário React controlado com validação inline e mensagens de erro.
Tratamento de Eventos e Renderização de Lista
Tratar eventos e renderizar listas dinâmicas com keys em React.
useState
Hook de gestão de estado.
useEffect
Hook de efeitos colaterais.
useContext
Estado compartilhado via context.
useReducer
Gestão de estado complexo.
useMemo
Memoizar resultados de computação.
useCallback
Memoizar funções de callback.
useRef
Referenciar DOM e valores mutáveis.
Hooks Personalizados
Extrair lógica reutilizável.
Comunicação de Componentes
Comunicação entre componentes pai-filho e irmãos.
Error Boundaries
Capturar erros de componentes.
Lazy Loading
Code splitting e lazy loading.
Portal
Renderizar para nós DOM fora do componente.
Componentes de Ordem Superior
Padrão de aprimoramento de componentes.
Render Props
Padrão de render props.
Otimização de Desempenho
Dicas de otimização de desempenho React.
Was this helpful?