Hooks customizados: extraindo lógica reutilizável
1. Fundamentos dos Hooks Customizados
Hooks customizados são funções JavaScript que utilizam hooks nativos do React (useState, useEffect, useContext, etc.) para encapsular lógica reutilizável entre componentes. Diferente de funções utilitárias tradicionais, hooks customizados podem manter estado interno e interagir com o ciclo de vida do React.
Por que criar hooks customizados?
- Eliminam duplicação de código entre componentes
- Separam responsabilidades de forma clara
- Facilitam testes e manutenção
- Permitem composição de lógicas complexas
Regras fundamentais:
1. Sempre chame hooks no nível superior da função (não dentro de loops, condições ou funções aninhadas)
2. Chame hooks apenas em componentes React ou outros hooks customizados
2. Criando seu Primeiro Hook Customizado
A estrutura básica segue a convenção useNomeDoHook e retorna um objeto ou array com estado e funções.
import { useState, useEffect } from 'react';
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 {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Erro ao salvar no localStorage:', error);
}
};
return [storedValue, setValue];
}
Uso prático:
function App() {
const [nome, setNome] = useLocalStorage('nome', '');
const [tema, setTema] = useLocalStorage('tema', 'claro');
return (
<div>
<input value={nome} onChange={(e) => setNome(e.target.value)} />
<button onClick={() => setTema(tema === 'claro' ? 'escuro' : 'claro')}>
Alternar Tema
</button>
</div>
);
}
3. Hooks de Estado e Efeitos Customizados
useDebounce - Otimizando Buscas em Tempo Real
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
useDocumentTitle - Gerenciando Título Dinâmico
import { useEffect } from 'react';
function useDocumentTitle(title, fallbackTitle = 'Meu App') {
useEffect(() => {
const previousTitle = document.title;
document.title = title || fallbackTitle;
return () => {
document.title = previousTitle;
};
}, [title, fallbackTitle]);
}
4. Hooks para Comunicação com APIs
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const source = axios.CancelToken.source();
const response = await axios.get(url, {
...options,
cancelToken: source.token,
});
setData(response.data);
} catch (err) {
if (!axios.isCancel(err)) {
setError(err.message || 'Erro na requisição');
}
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
5. Hooks de Interação com o Usuário
useMediaQuery - Responsividade em Tempo Real
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event) => setMatches(event.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
useClickOutside - Fechando Modais e Dropdowns
import { useEffect, useRef } from 'react';
function useClickOutside(handler) {
const ref = useRef(null);
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [handler]);
return ref;
}
6. Hooks com Contexto e Estado Global
useAuth - Autenticação Reutilizável
import { createContext, useContext, useState, useCallback } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = useCallback(async (credentials) => {
setLoading(true);
try {
const response = await api.post('/login', credentials);
setUser(response.data.user);
localStorage.setItem('token', response.data.token);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => {
setUser(null);
localStorage.removeItem('token');
}, []);
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth deve ser usado dentro de AuthProvider');
return context;
}
7. Boas Práticas e Composição de Hooks
Hooks customizados podem compor outros hooks, criando abstrações poderosas:
function useUserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'pt-BR');
const isMobile = useMediaQuery('(max-width: 768px)');
useDocumentTitle(`App - ${theme} mode`);
return {
theme,
setTheme,
language,
setLanguage,
isMobile,
};
}
Boas práticas essenciais:
- Testabilidade: Hooks puros (sem dependências externas) são mais fáceis de testar
- Memoização: Use useMemo e useCallback para evitar recriações desnecessárias
- Nomenclatura clara: Prefixo use e nomes descritivos
- Documentação: Comente parâmetros e retornos complexos
8. Padrões Avançados e Publicação
Hooks com Parâmetros Opcionais
function useTimer({ initialTime = 0, autoStart = false, onTick, onComplete }) {
const [time, setTime] = useState(initialTime);
const [isRunning, setIsRunning] = useState(autoStart);
useEffect(() => {
if (!isRunning) return;
const interval = setInterval(() => {
setTime((prev) => {
if (prev <= 0) {
clearInterval(interval);
onComplete?.();
return 0;
}
onTick?.(prev - 1);
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isRunning, onTick, onComplete]);
return { time, isRunning, start: () => setIsRunning(true), stop: () => setIsRunning(false) };
}
Publicação no npm:
1. Crie um pacote com npm init
2. Estruture os hooks em src/
3. Configure package.json com "main": "dist/index.js"
4. Use Babel/TypeScript para transpilação
5. Publique com npm publish
Debugging com React DevTools:
- Hooks customizados aparecem na árvore de componentes como useNomeDoHook
- Use useDebugValue para exibir informações personalizadas no DevTools
import { useDebugValue } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
Referências
- Documentação Oficial: Hooks Customizados — Guia completo da equipe React sobre criação e reutilização de hooks customizados.
- React Hooks: Como criar hooks customizados — Tutorial prático do freeCodeCamp com exemplos do mundo real.
- useHooks - Biblioteca de hooks customizados — Coleção curada de hooks reutilizáveis com exemplos e documentação.
- React Hooks Testing Library — Guia oficial para testar hooks customizados com React Testing Library.
- Padrões Avançados de Hooks no React — Artigo de Kent C. Dodds sobre boas práticas e testes de hooks.
- Publicando hooks no npm — Tutorial passo a passo para publicar hooks customizados como pacote npm.