Os React Hooks revolucionaram a forma de escrever componentes React. Desde o React 16.8, os Hooks permitem usar estado, ciclo de vida e contexto em componentes funcionais. Este guia completo de React Hooks cobre cada Hook com exemplos praticos.
useState: Gerenciar estado
useState e o Hook mais fundamental.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
</div>
);
}
// Lazy initialization — runs only on first render
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data') || '{}');
});
// Updating objects — always create a new object
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, name: 'Alice' }));
// Updating arrays — use spread or filter/map
const [items, setItems] = useState<string[]>([]);
setItems(prev => [...prev, 'new item']);
setItems(prev => prev.filter(item => item !== 'remove me'));useEffect: Efeitos colaterais e ciclo de vida
useEffect permite realizar efeitos colaterais.
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
// Runs when userId changes (componentDidMount + componentDidUpdate)
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
// Cleanup function (componentWillUnmount)
return () => { cancelled = true; };
}, [userId]); // dependency array
return <div>{user?.name}</div>;
}
// Run once on mount
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []); // empty dependency array
// Run on every render (rarely needed)
useEffect(() => {
console.log('Component rendered');
}); // no dependency arrayuseContext: Consumir contexto
useContext permite assinar o contexto React.
import { createContext, useContext, useState } from 'react';
// 1. Create a context with a default value
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
// 2. Create a provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Consume context with useContext
function ThemeButton() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('Must be inside ThemeProvider');
return (
<button onClick={ctx.toggleTheme}>
Current: {ctx.theme}
</button>
);
}
// 4. Custom hook for cleaner usage
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}useMemo: Memorizar calculos caros
useMemo armazena em cache o resultado de um calculo caro.
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// Only recalculates when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize a sorted copy
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<ul>
{sortedItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// Do NOT overuse useMemo — only for truly expensive computations
// Simple operations do not need memoization
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);useCallback: Memorizar funcoes
useCallback retorna uma versao memorizada de um callback.
import { useCallback, useState, memo } from 'react';
// Child component wrapped in memo — only re-renders if props change
const SearchInput = memo(({ onSearch }: { onSearch: (q: string) => void }) => {
console.log('SearchInput rendered');
return <input onChange={e => onSearch(e.target.value)} />;
});
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Without useCallback, a new function is created every render
// causing SearchInput to re-render unnecessarily
const handleSearch = useCallback((q: string) => {
setQuery(q);
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
}, []); // stable reference
return (
<div>
<SearchInput onSearch={handleSearch} />
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</div>
);
}useRef: Referencias mutaveis
useRef retorna um objeto ref mutavel.
import { useRef, useEffect, useState } from 'react';
function TextInputWithFocus() {
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // auto-focus on mount
}, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
// Track previous value
const prevTimeRef = useRef(time);
useEffect(() => { prevTimeRef.current = time; });
return (
<div>
<p>Time: {time}s (prev: {prevTimeRef.current}s)</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useReducer: Logica de estado complexa
useReducer e uma alternativa ao useState.
import { useReducer } from 'react';
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return { count: 0, step: 1 };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}Hooks personalizados: Logica reutilizavel
Os Hooks personalizados extraem logica em funcoes reutilizaveis.
// useLocalStorage — persist state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// useDebounce — debounce a rapidly changing value
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch — generic data fetching
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false); } })
.catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', '');
const debouncedName = useDebounce(name, 300);
const { data, loading } = useFetch<User[]>(`/api/search?q=${debouncedName}`);
}Regras dos Hooks
Duas regras essenciais para os Hooks.
- Chame Hooks apenas no nivel superior.
- Chame Hooks apenas de funcoes React.
// WRONG — Hook inside a condition
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // breaks Hook order
}
}
// CORRECT — condition inside the Hook
function Good({ isLoggedIn }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (isLoggedIn) fetchUser().then(setUser);
}, [isLoggedIn]);
}Armadilhas comuns e solucoes
Closures obsoletas no useEffect
Quando se referencia estado sem incluir nas dependencias.
// BUG: stale closure
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs 0 (stale!)
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // count is missing from dependencies
}
// FIX: use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // always uses latest value
}, 1000);
return () => clearInterval(id);
}, []); // safe with functional updateLoops infinitos com useEffect
Definir estado no useEffect sem dependencias corretas.
// BUG: infinite loop
useEffect(() => {
setCount(count + 1); // triggers re-render, which runs effect again
}); // no dependency array = runs every render
// FIX: add dependency array
useEffect(() => {
if (count < 10) setCount(count + 1);
}, [count]); // only runs when count changesDependencias de objeto/array
Objetos e arrays sao comparados por referencia.
// BUG: new object every render
function App() {
const options = { page: 1, limit: 10 }; // new ref each render
useEffect(() => {
fetchData(options);
}, [options]); // runs every render!
}
// FIX: useMemo to stabilize the reference
function App() {
const options = useMemo(() => ({ page: 1, limit: 10 }), []);
useEffect(() => {
fetchData(options);
}, [options]); // stable reference
}Perguntas frequentes
O que sao React Hooks?
Funcoes que permitem usar recursos do React em componentes funcionais.
Diferenca entre useMemo e useCallback?
useMemo memoriza um valor, useCallback memoriza uma funcao.
Quando usar useReducer?
Quando a logica de estado e complexa.
Hooks em componentes de classe?
Nao, apenas em componentes funcionais.
Como evitar loops infinitos?
Especificar sempre as dependencias corretas.
Os React Hooks sao essenciais para o desenvolvimento React moderno.