Custom Hooks
Extraia lógica reutilizável com estado em funções que começam com use - o principal mecanismo de reutilização de código do React.
Busque em todas as páginas da documentação
Extraia lógica reutilizável com estado em funções que começam com use - o principal mecanismo de reutilização de código do React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
// Padrão: um hook customizado é apenas uma função que chama outros hooks
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle] as const;
}
// Uso
const [isOpen, toggleOpen] = useToggle(false);Quando usar isso: Você se encontra duplicando a mesma combinação de useState, useEffect, useRef ou outros hooks em vários componentes.
"use client";
import { useCallback, useEffect, useState } from "react";
// Hook customizado: estado de local storage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Armazenamento cheio ou indisponível
}
}, [key, value]);
const remove = useCallback(() => {
setValue(initialValue);
localStorage.removeItem(key);
}, [key, initialValue]);
return [value, setValue, remove] as const;
}
// Componente usando o hook customizado
export function Preferences() {
const [name, setName, clearName] = useLocalStorage("user-name", "");
const [darkMode, setDarkMode] = useLocalStorage("dark-mode", false);
return (
<div className={`space-y-4 p-4 rounded ${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}>
<div>
<label className="block text-sm font-medium mb-1">Nome</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="border rounded px-3 py-2 text-black"
placeholder="Digite seu nome"
/>
</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={darkMode}
onChange={(e) => setDarkMode(e.target.checked)}
/>
<span className="text-sm">Modo escuro</span>
</label>
<div className="flex gap-2">
<button onClick={clearName} className="text-sm text-blue-500 underline">
Limpar nome
</button>
</div>
{name && <p className="text-sm">Olá, {name}!</p>}
</div>
);
}O que isso demonstra:
useLocalStorage) que compõe useState, useEffect e useCallbacktypeof windowas const restringe o tipo de retorno de Array para uma tupla específicause e chama outros hooksuse é obrigatório - ele sinaliza ao React (e às ferramentas de linting) que a função segue as regras dos hooks| Padrão | Convenção | Exemplo |
|---|---|---|
| Valor único | Retorna o valor diretamente | useOnlineStatus() → boolean |
| Valor + setter | Retorna uma tupla [valor, setter] | useToggle() → [boolean, () => void] |
| Múltiplos valores | Retorna um objeto | useFetch() → { data, error, loading } |
| Apenas ações | Retorna um objeto de funções | useClipboard() → { copy, paste } |
useDebounce - debouncing de um valor que muda rapidamente:
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;
}
// Uso
const debouncedQuery = useDebounce(query, 300);useFetch - busca de dados com estados de loading e erro:
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((json) => { setData(json); setError(null); })
.catch((err) => { if (err.name !== "AbortError") setError(err); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}useMediaQuery - breakpoints responsivos:
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mql = window.matchMedia(query);
setMatches(mql.matches);
function handler(e: MediaQueryListEvent) {
setMatches(e.matches);
}
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
// Uso
const isMobile = useMediaQuery("(max-width: 768px)");useClickOutside - detectar cliques fora de um ref:
function useClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
handler();
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [ref, handler]);
}usePrevious - rastrear o valor anterior:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}// Use genéricos para hooks reutilizáveis
function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] { ... }
// Use `as const` para retornos de tupla para que os tipos de desestruturação estejam corretos
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle] as const;
// Tipo de retorno: readonly [boolean, () => void]
// Sem `as const`: (boolean | (() => void))[]
}
// Use sobrecargas para hooks com múltiplas assinaturas de chamada
function useControllable<T>(value: T): [T, (v: T) => void];
function useControllable<T>(value: undefined, defaultValue: T): [T, (v: T) => void];
function useControllable<T>(value: T | undefined, defaultValue?: T) {
const [internal, setInternal] = useState(defaultValue ?? value!);
if (value !== undefined) return [value, () => {}] as const;
return [internal, setInternal] as const;
}Não começar com use - Se o seu hook se chama getToggle em vez de useToggle, o linter não aplicará as regras dos hooks, levando a bugs sutis. Correção: Sempre prefixe hooks customizados com use.
Chamar hooks condicionalmente dentro de hooks customizados - As regras dos hooks também se aplicam dentro de hooks customizados. Correção: Nunca coloque useState ou useEffect dentro de um bloco if ou após um retorno antecipado.
Retornar referências instáveis - Retornar um novo objeto { value, toggle } a cada renderização faz com que as dependências useEffect dos consumidores mudem a cada vez. Correção: Use useMemo para estabilizar objetos ou retorne uma tupla.
Super-abstração - Criar um hook customizado para lógica usada em apenas um componente adiciona indireção sem benefício. Correção: Extraia para um hook customizado apenas quando a lógica for usada em 2+ componentes ou quando melhorar a legibilidade de um componente complexo.
Limpeza ausente - Esquecer de limpar assinaturas, timers ou listeners de eventos em seu hook customizado causa vazamentos de memória. Correção: Sempre retorne uma função de limpeza de useEffect dentro do seu hook.
Closures obsoletas em callbacks retornados - Callbacks retornados de hooks customizados podem fechar sobre estado obsoleto se não forem envolvidos em useCallback com as dependências corretas. Correção: Use useCallback para quaisquer funções que você retornar, ou use o padrão de atualizador.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Render props | Você precisa compartilhar lógica de renderização de UI, não apenas estado | Você só precisa compartilhar lógica com estado |
| Componentes de ordem superior (HOC) | Código legado requer encapsulamento de componentes | Começando código novo - hooks são mais simples |
| Funções utilitárias | A lógica é pura (sem hooks, sem estado, sem efeitos) | A lógica envolve estado ou ciclo de vida do React |
| Context + Provider | O estado compartilhado precisa ser acessível por toda a subárvore | Cada consumidor precisa de estado independente |
| Hooks de terceiros (react-use, usehooks-ts) | Uma implementação bem testada já existe | Seu caso de uso é único para o seu domínio |
Quando extrair um hook customizado: Se você tem 2+ componentes com a mesma combinação useState + useEffect, ou se a lógica de hooks de um componente excede ~15 linhas e tem uma responsabilidade clara, extraia-a.
De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: Hook de autenticação com sessão + assinatura em tempo real
// Arquivo: src/hooks/use-auth.ts
'use client'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { type User } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase/client'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const getSession = async () => {
try {
const { data: { session } } = await supabase.auth.getSession()
setUser(session?.user ?? null)
} catch (error) {
console.error('Erro ao obter sessão:', error)
} finally {
setLoading(false)
}
}
getSession()
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setUser(session?.user ?? null)
setLoading(false)
}
)
return () => { subscription.unsubscribe() }
}, [])
const signOut = useCallback(async () => {
try {
await supabase.auth.signOut()
setUser(null)
} catch (error) {
console.error('Erro ao sair:', error)
}
}, [])
return useMemo(() => ({
user,
loading,
signOut,
isAuthenticated: user !== null,
userId: user?.id || null,
}), [user, loading, signOut])
}O que isso demonstra em produção:
subscription.unsubscribe() previne vazamentos de memória quando o componente é desmontadouseCallback em signOut cria uma referência estável para que os consumidores que o usam em arrays de dependência não executem efeitos repetidamenteuseMemo no objeto de retorno previne re-renderizações desnecessárias. Sem ele, uma nova referência de objeto é criada a cada renderização, mesmo que os valores sejam os mesmosisAuthenticated: user !== null é um valor derivado computado a partir do estado, não armazenado separadamenteuseAuth() obtém seu próprio estado, mas todos sincronizam via onAuthStateChangeuse e chama outros hooks (useState, useEffect, etc.) internamente.use.// Sem `as const`: (boolean | (() => void))[]
// Com `as const`: readonly [boolean, () => void]
return [value, toggle] as const;as const restringe o tipo de retorno a uma tupla específica, permitindo tipos de desestruturação corretos.use.function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initial;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
// ...
}typeof window === "undefined" ou mantenha-o dentro de useEffect.{ value, toggle } cria uma nova referência de objeto a cada renderização.useEffect, o efeito será executado a cada renderização.useMemo ou retornando uma tupla em vez disso.function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initial;
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initial;
});
// ...
return [value, setValue];
}<T> para tornar o hook reutilizável com qualquer tipo de dado.useCallback para fornecer referências estáveis.useCallback, os consumidores que usam a função em arrays de dependência recebem uma nova referência a cada renderização.setState(prev => ...)) dentro de useCallback para minimizar dependências.renderHook de @testing-library/react para renderizar o hook sem um componente.act().useAuth pode chamar useLocalStorage, que chama useState e useEffect.[value, setter]) permitem que os consumidores renomeiem as variáveis: const [name, setName] = useLocalStorage(...).{ data, error, loading }) são melhores quando há muitos valores de retorno e a ordem não importa.Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥