Os React Hooks transformaram fundamentalmente a forma como desenvolvemos componentes React. Introduzidos na versão 16.8, eles permitem usar estado e outras funcionalidades do React em componentes funcionais, eliminando a necessidade de classes na maioria dos casos.
useState: Gerenciando Estado Local
O useState é o hook mais básico e fundamental para gerenciar estado em componentes funcionais:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const increment = () => setCount(count + 1);
const decrement = () => setCount(prev => prev - 1); // Forma funcional
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Digite seu nome"
/>
<p>Olá {name}, contador: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Padrões Avançados com useState
// Estado complexo com objeto
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true
}
});
const updateUser = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
const updatePreference = (key, value) => {
setUser(prev => ({
...prev,
preferences: {
...prev.preferences,
[key]: value
}
}));
};
return (
// JSX do componente
);
}
useEffect: Efeitos Colaterais
O useEffect substitui componentDidMount, componentDidUpdate e componentWillUnmount em uma única API:
import React, { useState, useEffect } from 'react';
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (!cancelled) {
setUser(userData);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]); // Dependency array
if (loading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error}</div>;
if (!user) return <div>Usuário não encontrado</div>;
return <div>{user.name}</div>;
}
Padrões de useEffect
// Efeito que roda apenas uma vez (componentDidMount)
useEffect(() => {
console.log('Componente montado');
}, []);
// Efeito que roda a cada render
useEffect(() => {
console.log('Componente renderizado');
});
// Efeito com cleanup (componentWillUnmount)
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
return () => clearInterval(timer);
}, []);
// Efeito condicional
useEffect(() => {
if (user && user.id) {
trackUserActivity(user.id);
}
}, [user]);
useContext: Compartilhando Estado
O useContext simplifica o consumo de Context API:
// Criando o contexto
const ThemeContext = React.createContext();
// Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumindo o contexto
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
className={`btn btn-${theme}`}
onClick={toggleTheme}
>
Tema atual: {theme}
</button>
);
}
useReducer: Estado Complexo
Para estado mais complexo, useReducer oferece mais controle:
// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// Componente usando useReducer
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
});
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
return (
// JSX do componente
);
}
Hooks Customizados
Criar hooks customizados permite reutilizar lógica entre componentes:
// Hook para requisições HTTP
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Hook para localStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Erro ao ler localStorage:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Erro ao salvar no localStorage:', error);
}
};
return [storedValue, setValue];
}
// Uso dos hooks customizados
function UserProfile() {
const { data: user, loading, error } = useApi('/api/user');
const [preferences, setPreferences] = useLocalStorage('userPrefs', {});
if (loading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Tema: {preferences.theme || 'padrão'}</p>
</div>
);
}
useMemo e useCallback: Otimização
Para otimização de performance, use useMemo e useCallback:
import React, { useState, useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, filter }) {
// Memoiza cálculo custoso
const filteredItems = useMemo(() => {
console.log('Filtrando itens...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoiza função para evitar re-renders desnecessários
const handleItemClick = useCallback((itemId) => {
console.log('Item clicado:', itemId);
// Lógica do click
}, []); // Sem dependências = função nunca muda
const handleItemUpdate = useCallback((itemId, newData) => {
// Lógica de atualização
}, [items]); // Recria apenas quando items mudam
return (
<div>
{filteredItems.map(item => (
<ItemComponent
key={item.id}
item={item}
onClick={handleItemClick}
onUpdate={handleItemUpdate}
/>
))}
</div>
);
}
useRef: Referências e Valores Mutáveis
O useRef serve para acessar elementos DOM e manter valores mutáveis:
function FocusInput() {
const inputRef = useRef(null);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`Render #${renderCount.current}`);
});
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focar Input</button>
<p>Renders: {renderCount.current}</p>
</div>
);
}
Regras dos Hooks
Os Hooks têm regras importantes que devem ser seguidas:
1. Apenas no Nível Superior
// ❌ Não faça isso
function BadComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // Erro!
}
return <div>{state}</div>;
}
// ✅ Faça isso
function GoodComponent({ condition }) {
const [state, setState] = useState(0);
if (!condition) {
return null;
}
return <div>{state}</div>;
}
2. Apenas em Componentes React
// ❌ Não use hooks em funções regulares
function regularFunction() {
const [state, setState] = useState(0); // Erro!
}
// ✅ Use em componentes ou hooks customizados
function MyComponent() {
const [state, setState] = useState(0); // OK
}
function useMyHook() {
const [state, setState] = useState(0); // OK
}
Padrões Avançados
Hook para Debounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Uso
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Fazer busca apenas após 500ms de inatividade
performSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Buscar..."
/>
);
}
Testing com Hooks
Para testar componentes com hooks, use React Testing Library:
import { render, screen, fireEvent } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
// Testando componente
test('counter increments when button is clicked', () => {
render(<Counter />);
const button = screen.getByText('+');
const counter = screen.getByText(/contador: 0/);
fireEvent.click(button);
expect(screen.getByText(/contador: 1/)).toBeInTheDocument();
});
// Testando hook customizado
test('useCounter hook', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Conclusão
Os React Hooks representam uma mudança paradigmática no desenvolvimento React, oferecendo uma API mais simples e poderosa para gerenciar estado e efeitos colaterais. Eles promovem reutilização de lógica, simplificam testes e tornam os componentes mais legíveis.
A chave para dominar hooks está em entender quando usar cada um, como criar hooks customizados eficazes e seguir as regras fundamentais. Com essas práticas, você pode criar aplicações React mais maintíveis e performáticas.