Sobre nós Guias Projetos Contactos
Админка
please wait

Os React Hooks transformaram a forma como escrevemos componentes React, permitindo funcionalidades de estado e de ciclo de vida em componentes funcionais. Compreender os hooks em profundidade — para além de useState e useEffect — desbloqueia padrões poderosos para criar aplicações React fáceis de manter. Este guia aborda como dominar os React Hooks na perspetiva de um programador sénior.

Porque é que os Hooks Importam

Os hooks revolucionaram o desenvolvimento em React:

  1. Componentes mais simples: Chega de boilerplate de classes
  2. Reutilização de lógica: Hooks personalizados partilham lógica com estado
  3. Composição: Combine pequenos hooks em comportamentos complexos
  4. Testabilidade: Os hooks são mais fáceis de testar do que métodos de ciclo de vida
  5. TypeScript: Melhor inferência de tipos do que componentes de classe

Hooks Essenciais

useState: Estado do Componente

import { useState } from 'react';
function Counter() {
// Utilização básica
const [count, setCount] = useState(0);
// Inicialização lazy (cálculo dispendioso)
const [data, setData] = useState(() => {
return computeExpensiveInitialValue();
});
// Estado de objeto
const [user, setUser] = useState({ name: '', email: '' });
// Atualizar com callback (aceder ao valor anterior)
const increment = () => {
setCount(prev => prev + 1);
};
// Atualizar objeto (spread para manter os outros campos)
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
</div>
);
}

useEffect: Efeitos Secundários

import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Executar em cada renderização (raramente necessário)
useEffect(() => {
console.log('Component rendered');
});
// Executar uma vez no mount
useEffect(() => {
console.log('Component mounted');
// Cleanup no unmount
return () => {
console.log('Component unmounted');
};
}, []);
// Executar quando a dependência muda
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Evitar a atualização de estado se o componente tiver sido desmontado
if (!cancelled) {
setUser(data);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup: cancelar pedido pendente
return () => {
cancelled = true;
};
}, [userId]); // Voltar a executar quando userId muda
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}

useContext: Estado Partilhado

import { createContext, useContext, useState } from 'react';
// Criar context
const ThemeContext = createContext(null);
// Componente 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>
);
}
// Hook personalizado para consumir o context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Utilização
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}

useReducer: Estado Complexo

import { useReducer } from 'react';
// Função reducer
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
case 'CLEAR_COMPLETED':
return state.filter(todo => !todo.done);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'ADD', text });
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
Clear Completed
</button>
</div>
);
}

Hooks de Performance

useMemo: Memorizar Valores

import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }) {
// Recalcular apenas quando items ou filter mudam
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memorizar objetos complexos para props de componentes filhos
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>
);
}

useCallback: Memorizar Funções

import { useCallback, useState, memo } from 'react';
// Componente filho memorizado
const Button = memo(function Button({ onClick, label }) {
console.log(`Rendering ${label}`);
return <button onClick={onClick}>{label}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Sem useCallback, isto cria uma nova função em cada renderização
// const handleClick = () => setCount(c => c + 1);
// Com useCallback, a referência da função mantém-se estável
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// Se o callback precisar do estado atual, inclua-o nas dependências
const handleReset = useCallback(() => {
setCount(0);
setName('');
}, []);
return (
<div>
<p>Count: {count}</p>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/*O Button não voltará a renderizar quando name mudar*/}
<Button onClick={handleClick} label="Increment" />
<Button onClick={handleReset} label="Reset" />
</div>
);
}

useRef: Referência Mutável

import { useRef, useEffect, useState } from 'react';
function FocusInput() {
const inputRef = useRef(null);
// Focar no mount
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={stop}>Stop</button>
</div>
);
}
// Acompanhar o valor anterior
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Component({ value }) {
const prevValue = usePrevious(value);
return (
<p>
Current: {value}, Previous: {prevValue}
</p>
);
}

Hooks Personalizados

Hook de Obtenção de Dados

import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Utilização
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Hook de Local Storage

import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Inicializar a partir de localStorage ou usar o valor inicial
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Sincronizar com localStorage quando o valor muda
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// Utilização
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<input
type="range"
min="12"
max="24"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</div>
);
}

Hook de Debounce

import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Utilização: Pesquisa com debounce
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
const { data: results } = useFetch(
debouncedSearch ? `/api/search?q=${debouncedSearch}` : null
);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<ul>
{results?.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

Hook do Tamanho da Janela

import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Utilização
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
);
}

Regras dos Hooks

  1. Chamar apenas ao nível superior: Nunca em ciclos, condições ou funções aninhadas
  2. Chamar apenas em funções React: Componentes ou hooks personalizados
  3. Nomear hooks personalizados com «use»: Permite a verificação pela regra do linter
// MAU
function Component({ condition }) {
if (condition) {
const [value, setValue] = useState(0); // Erro!
}
}
// BOM
function Component({ condition }) {
const [value, setValue] = useState(0);
if (condition) {
// Use o valor aqui
}
}

Principais Conclusões

  1. useState para estado simples: Objetos precisam de spread para atualizações
  2. useEffect com cleanup: Evite fugas de memória e condições de corrida
  3. useCallback/useMemo com cuidado: A otimização prematura é dispendiosa
  4. Hooks personalizados para reutilização: Extraia padrões comuns
  5. useReducer para estado complexo: Melhor do que múltiplos useState
  6. Os arrays de dependências importam: Dependências em falta causam bugs

Os hooks são a base do React moderno — domine-os e escreverá componentes mais limpos e fáceis de manter.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive