React: Ganchos personalizados
Última atualização: 2026-08-26
1. O que você vai aprender
- Convenções e regras de nomenclatura para ganchos personalizados
- Definição dos parâmetros e valores de retorno dos hooks personalizados
- 6 ganchos personalizados práticos
- Princípios para a reutilização de hooks personalizados
2. A história de um requisito de reutilização de formulários
Padrões de reutilização para ganchos personalizados
flowchart TD
A[Repetitive Logic] --> B[Extract as useXxx]
B --> C[ComponentsA useXxx]
B --> D[ComponentsB useXxx]
B --> E[ComponentsC useXxx]
F[useXxxInside] --> G[useState]
F --> H[useEffect]
F --> I[useRef]
F --> J[useContext]
K[The Rule of Three] --> L{Number of logical repetitions?}
L -->|1 time| M[No extraction required]
L -->|2 times| N[Consider extracting]
L -->|3+ times| O[Must be extracted ✅]
style B fill:#e8f5e9,stroke:#2e7d32
style O fill:#c8e6c9,stroke:#2e7d32
(1) Problema: A mesma lógica se repete em 5 páginas
Bob precisa “recuperar as preferências do usuário do armazenamento local” em cinco páginas diferentes:
// Page A:Settings Page
function SettingsPage() {
const [prefs, setPrefs] = useState(() => {
const saved = localStorage.getItem('preferences')
return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
})
useEffect(() => {
localStorage.setItem('preferences', JSON.stringify(prefs))
}, [prefs])
// ... Page A specifically UI
}
// Page B:Dashboard
function Dashboard() {
const [prefs, setPrefs] = useState(() => {
const saved = localStorage.getItem('preferences')
return saved ? JSON.parse(saved) : { theme: 'light', lang: 'zh' }
})
useEffect(() => {
localStorage.setItem('preferences', JSON.stringify(prefs))
}, [prefs])
// ... Page B specifically UI(Exactly the same logic!)
}
Problema: localStorage A lógica de leitura/gravação foi copiada e colada cinco vezes em cinco páginas. Se quisermos adicionar um recurso para “monitorar eventos de armazenamento”, teríamos que modificar cinco arquivos.
(2) Soluções para ganchos personalizados
// ---- Custom Hook:Packaging localStorage Reading and Writing ----
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key)
return saved ? JSON.parse(saved) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (e) {
console.error('Save to localStorage Failure:', e)
}
}, [key, value])
return [value, setValue]
}
// ---- Use this consistently on all pages ----
function SettingsPage() {
const [prefs, setPrefs] = useLocalStorage('preferences', {
theme: 'light', lang: 'zh'
})
// ... Specifically UI
}
function Dashboard() {
const [prefs, setPrefs] = useLocalStorage('preferences', {
theme: 'light', lang: 'zh'
})
// ... Specifically UI
}
Vantagens: 20 linhas de lógica → 1 linha de código. Ao modificar a lógica, basta alterar um único arquivo: useLocalStorage.
3. Regras para ganchos personalizados
| Regra | Descrição | Consequências da violação |
|---|---|---|
Começando com use |
Identificando hooks pelos nomes das dependências do React | O linter não verifica esta regra; chamadas condicionais não geram erros |
| Chame apenas no nível superior | Não chame dentro de loops, condicionais ou blocos aninhados | Desalinhamento da cadeia de hooks, confusão de estado |
| Chame apenas dentro de componentes de função ou hooks personalizados | Não utilize dentro de funções comuns ou componentes de classe | Erro de tempo de execução: Chamada de hook inválida |
| Retorna dados/funções, não JSX | Hooks retornam lógica, componentes retornam interface do usuário | Viola a separação de interesses, dificultando a reutilização |
| Limpeza de efeitos colaterais | Função de limpeza no retorno do useEffect | Vazamentos de memória, acúmulo de ouvintes de eventos |
(1) Convenções de nomenclatura
// ✅ It is essential to use Introduction
function useOnlineStatus() { }
function useWindowSize() { }
function useDebounce() { }
// ❌ Not based on use Introduction → React Will not check Hooks Rules
function onlineStatus() { }
function fetchData() { }
(2) Projeto do valor de retorno
// Method 1:Return an array(like useState)
function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle] // Array,Flexible Deconstruction of Names
}
// Method 2:Return Object(Greater clarity when there are multiple return values)
function useUser() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
return { user, loading, error } // Object,Property Names Are Clear
}
// Method 3:Returns a single value
function useDocumentTitle(title) {
useEffect(() => { document.title = title }, [title])
// No return value required
}
(3) Qualquer hook pode ser usado internamente
function useData(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Request Failed')
return res.json()
})
.then(data => { if (!cancelled) { setData(data); setLoading(false) }})
.catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Used internally useState + useEffect + useCallback(If necessary)
// Custom Hook You can combine any of the built-in features Hooks
4. 6 ganchos personalizados práticos
| Hook | Função | Hooks usados internamente | Valor de retorno |
|---|---|---|---|
useToggle |
Interruptor booleano | useState, useCallback |
{ value, toggle, setTrue, setFalse } |
useDebounce |
Entrada com estabilização de imagem | useState, useEffect |
debouncedValue |
useWindowSize |
Monitoramento do tamanho da janela | useState, useEffect |
{ width, height } |
useFetch |
Solicitação de dados de três estados | useState, useEffect, useCallback |
{ data, loading, error, refetch } |
useIntersectionObserver |
Detecção de visibilidade | useState, useEffect |
isIntersecting |
useMediaQuery |
Ponto de quebra responsivo | useState, useEffect |
matches |
(1) Hook 1: useToggle (botão de alternância)
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue(v => !v), [])
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
return { value, toggle, setTrue, setFalse }
}
// Usage
function ModalExample() {
const { value: isOpen, setTrue: open, setFalse: close } = useToggle(false)
return (
<div>
<button onClick={open}>Open the pop-up window</button>
{isOpen && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ backgroundColor: 'white', padding: '24px', borderRadius: '8px' }}>
<h2>Pop-up Title</h2>
<p>Pop-up Content</p>
<button onClick={close}>Close</button>
</div>
</div>
)}
</div>
)
}
(2) Hook 2: useDebounce (anti-jitter)
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// Usage:Search Input Stabilization
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
// Only at debouncedQuery Request only when there is a change(The user has stopped typing 500ms after )
useEffect(() => {
if (debouncedQuery) {
fetch('/api/search?q=' + debouncedQuery).then(/* ... */)
}
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
}
(3) Hook 3: useWindowSize (tamanho da janela)
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
let timeoutId
function handleResize() {
// Image Stabilization:50ms several times inside resize Execute only once
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
setSize({ width: window.innerWidth, height: window.innerHeight })
}, 50)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
clearTimeout(timeoutId)
}
}, [])
return size
}
// Usage
function ResponsiveComponent() {
const { width } = useWindowSize()
return (
<p>
Current window width:{width}px
{width < 768 ? '(Mobile View)' : width < 1024 ? '(Flat View)' : '(Desktop View)'}
</p>
)
}
(4) Hook 4: useFetch (solicitação de dados)
function useFetch(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetch(url, options)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
return res.json()
})
.then(data => { if (!cancelled) { setData(data); setLoading(false) }})
.catch(err => { if (!cancelled) { setError(err.message); setLoading(false) }})
return () => { cancelled = true }
}, [url])
// Resubmit Request
const refetch = useCallback(() => {
setLoading(true)
fetch(url, options)
.then(res => res.json())
.then(data => { setData(data); setLoading(false) })
.catch(err => { setError(err.message); setLoading(false) })
}, [url])
return { data, loading, error, refetch }
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useFetch(`/api/users/${userId}`)
if (loading) return <p>Loading......</p>
if (error) return <p>Error:{error} <button onClick={refetch}>Retry</button></p>
return <h1>{user.name}</h1>
}
(5) Hook 5: useIntersectionObserver (Detecção de visibilidade)
function useIntersectionObserver(ref, options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
if (!ref.current) return
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting)
}, { threshold: options.threshold || 0.1, ...options })
observer.observe(ref.current)
return () => observer.disconnect()
}, [ref, options.threshold])
return isIntersecting
}
// Usage:Lazy Loading of Images
function LazyImage({ src, alt }) {
const imgRef = useRef(null)
const isVisible = useIntersectionObserver(imgRef)
const [loaded, setLoaded] = useState(false)
return (
<div ref={imgRef} style={{ minHeight: '200px', backgroundColor: '#f5f5f5' }}>
{isVisible && (
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{ width: '100%', opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
)}
</div>
)
}
(6) Hook 6: useMediaQuery (Pontos de quebra responsivos)
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches)
useEffect(() => {
const mql = window.matchMedia(query)
function handler(e) { setMatches(e.matches) }
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
// Usage
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
return (
<div>
{isMobile && <MobileMenu />}
{isDesktop && <FullSidebar />}
<p>
{isMobile ? '📱 Cell Phone' : isTablet ? '📟 Tablet' : '💻 Computer'}
</p>
</div>
)
}
5. Princípios para reutilização de hooks personalizados
| Princípio | Explicação | Exemplo incorreto | Exemplo correto |
|---|---|---|---|
| Responsabilidade Única | Um hook faz apenas uma coisa | useUserAndTheme() |
useUser() + useTheme() |
| Parâmetros flexíveis | Forneça valores padrão razoáveis | useFetch('/api/data') URL codificada |
useFetch(url) URL parametrizada |
| Valores de retorno completos | Retorna três estados: carregando, erro e dados | useFetch() Retorna apenas dados |
useFetch() Retorna { data, loading, error } |
| Limpeza de recursos | Limpa os efeitos colaterais quando o componente é desmontado | Não limpa temporizadores/ouvintes de eventos | Limpeza retornada por useEffect |
| Modular | Ganchos pequenos podem ser combinados para formar ganchos maiores | useComplexData() 100 linhas |
Combinação useFetch() + useDebounce() |
▶ Exemplo: Combinando vários ganchos pequenos
// ============================================
// Example:Combine multiple small Hook Building Complex Search Features
// ============================================
// 1. Small hook:Input Stabilization
function useDebounce(value, delay) { /* ... */ }
// 2. Small hook:Data Request
function useFetch(url) { /* ... */ }
// 3. Small hook:Local Storage
function useLocalStorage(key, initial) { /* ... */ }
// 4. Combined into a large Hook:Search
function useSearch(defaultQuery = '') {
const [query, setQuery] = useState(defaultQuery)
const [history, setHistory] = useLocalStorage('search_history', [])
const debouncedQuery = useDebounce(query, 500)
const { data, loading, error } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
)
function search(newQuery) {
setQuery(newQuery)
// Save Search History
if (newQuery.trim()) {
setHistory(prev => [newQuery, ...prev.filter(h => h !== newQuery)].slice(0, 10))
}
}
return { query, setQuery: search, results: data, loading, error, history }
}
// useSearch Combined internally useState + useDebounce + useFetch + useLocalStorage
6. Exemplo completo: Galeria de imagens (combinando vários ganchos personalizados)
// ============================================
// Complete Example:Photo Gallery(Combination 3 Custom Hook)
// ============================================
import { useState, useEffect, useCallback, useRef } from 'react'
// ---- Custom Hook 1:Infinite Scrolling ----
function useInfiniteScroll(callback) {
const sentinelRef = useRef(null)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) callback()
}, { threshold: 0.1 })
observer.observe(sentinel)
return () => observer.disconnect()
}, [callback])
return sentinelRef
}
// ---- Custom Hook 2:Keyboard Navigation ----
function useKeyPress(targetKey, callback) {
useEffect(() => {
function handler(e) {
if (e.key === targetKey) callback(e)
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [targetKey, callback])
}
// ---- Custom Hook 3:Click here ----
function useClickOutside(callback) {
const ref = useRef(null)
useEffect(() => {
function handler(e) {
if (ref.current && !ref.current.contains(e.target)) {
callback()
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [callback])
return ref
}
// ---- Image Gallery Component ----
function ImageGallery() {
const [images, setImages] = useState([])
const [page, setPage] = useState(1)
const [selected, setSelected] = useState(null)
const [loading, setLoading] = useState(false)
// Loading image
const loadMore = useCallback(() => {
if (loading) return
setLoading(true)
const newImages = Array.from({ length: 8 }, (_, i) => ({
id: images.length + i + 1,
url: `https://picsum.photos/seed/${images.length + i + 1}/300/200`,
title: `Image ${images.length + i + 1}`
}))
setTimeout(() => {
setImages(prev => [...prev, ...newImages])
setPage(p => p + 1)
setLoading(false)
}, 800)
}, [loading, images.length])
// First Load
useEffect(() => { loadMore() }, [])
// Infinite Scroll: Load More
const sentinelRef = useInfiniteScroll(loadMore)
// Click here to close the preview
const previewRef = useClickOutside(() => setSelected(null))
// Escape Key to close the preview
useKeyPress('Escape', () => setSelected(null))
// Use the left and right arrow keys to switch between images
useKeyPress('ArrowLeft', () => {
if (selected) {
const idx = images.findIndex(i => i.id === selected.id)
if (idx > 0) setSelected(images[idx - 1])
}
})
useKeyPress('ArrowRight', () => {
if (selected) {
const idx = images.findIndex(i => i.id === selected.id)
if (idx < images.length - 1) setSelected(images[idx + 1])
}
})
return (
<div style={{ maxWidth: '900px', margin: '0 auto' }}>
<h2>🖼️ Photo Gallery</h2>
<p style={{ color: '#666', fontSize: '13px' }}>Loaded {images.length} images | Keyboard:← Previous → Next Escape Close</p>
{/* Image Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
{images.map(img => (
<div key={img.id} onClick={() => setSelected(img)}
style={{ cursor: 'pointer', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<img src={img.url} alt={img.title}
style={{ width: '100%', height: '150px', objectFit: 'cover', transition: 'transform 0.3s' }}
onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'} />
<p style={{ padding: '8px', margin: 0, fontSize: '13px', textAlign: 'center' }}>{img.title}</p>
</div>
))}
</div>
{/* Load More Triggers */}
<div ref={sentinelRef} style={{ textAlign: 'center', padding: '20px' }}>
{loading ? <p>⏳ Loading......</p> : <p style={{ color: '#999' }}>↓ Scroll to load more</p>}
</div>
{/* Image Preview(Click here to close) */}
{selected && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.8)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000
}}>
<div ref={previewRef} style={{ maxWidth: '80vw', maxHeight: '80vh' }}>
<img src={selected.url.replace('300/200', '600/400')} alt={selected.title}
style={{ maxWidth: '100%', maxHeight: '80vh', borderRadius: '8px' }} />
<p style={{ color: 'white', textAlign: 'center', marginTop: '12px' }}>
{selected.title}(Click here to close)
</p>
</div>
</div>
)}
</div>
)
}
Resultado esperado: Uma galeria de imagens que carrega 8 imagens na abertura inicial; ao rolar até o final da página, mais imagens são carregadas automaticamente; clicar em uma imagem abre uma pré-visualização (com suporte à navegação por teclado); clicar fora da área de pré-visualização fecha a janela.
▶ Exemplo 2: Hook useClipboard — Copiar para a área de transferência
function useClipboard() {
const [copied, setCopied] = useState(false)
async function copy(text) {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
setCopied(false)
}
}
return { copied, copy }
}
function CopyButton({ text }) {
const { copied, copy } = useClipboard()
return (
<button onClick={() => copy(text)}
style={{ padding: '4px 12px', cursor: 'pointer', background: copied ? '#52c41a' : '#f0f0f0', color: copied ? 'white' : '#333', border: 'none', borderRadius: 4 }}>
{copied ? 'Copied!' : 'Copy'}
</button>
)
}
▶ Exemplo 3: Hook useOnlineStatus — Detecção do status da rede
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
function handleOnline() { setIsOnline(true) }
function handleOffline() { setIsOnline(false) }
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return isOnline
}
function StatusBar() {
const isOnline = useOnlineStatus()
return (
<div style={{ padding: '8px 16px', background: isOnline ? '#f6ffed' : '#fff2f0', borderBottom: `1px solid ${isOnline ? '#b7eb8f' : '#ffccc7'}` }}>
{isOnline ? '🟢 Online' : '🔴 Offline — changes will sync when reconnected'}
</div>
)
}
▶ Exemplo 4: Hook useLocalStorage — Persistência de estado
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key)
return saved !== null ? JSON.parse(saved) : initialValue
} catch { return initialValue }
})
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(value)) }
catch (e) { console.error('localStorage save failed:', e) }
}, [key, value])
return [value, setValue]
}
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
style={{
padding: '8px 16px', cursor: 'pointer', borderRadius: 4, border: 'none',
background: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333',
}}>
{theme === 'dark' ? '🌙 Dark' : '☀️ Light'} (saved)
</button>
)
}
▶ Exemplo 5: Hook useCountdown — Contagem regressiva
function useCountdown(seconds, autostart = false) {
const [timeLeft, setTimeLeft] = useState(seconds)
const [isRunning, setIsRunning] = useState(autostart)
useEffect(() => {
if (!isRunning || timeLeft <= 0) return
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) { setIsRunning(false); return 0 }
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [isRunning, timeLeft])
function start() { setTimeLeft(seconds); setIsRunning(true) }
function pause() { setIsRunning(false) }
function resume() { setIsRunning(true) }
const minutes = Math.floor(timeLeft / 60)
const secs = timeLeft % 60
const formatted = `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
return { timeLeft, formatted, isRunning, start, pause, resume }
}
function CountdownTimer() {
const { formatted, isRunning, start, pause, resume } = useCountdown(300)
return (
<div style={{ textAlign: 'center', padding: 20 }}>
<p style={{ fontSize: 48, fontFamily: 'monospace', margin: '0 0 16px' }}>{formatted}</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
{!isRunning ? <button onClick={start} style={{ padding: '8px 16px', cursor: 'pointer' }}>Start</button>
: <button onClick={pause} style={{ padding: '8px 16px', cursor: 'pointer' }}>Pause</button>}
{isRunning || <button onClick={resume} style={{ padding: '8px 16px', cursor: 'pointer' }}>Resume</button>}
</div>
</div>
)
}
❓ Perguntas Frequentes
P: Qual é a diferença entre um hook personalizado e uma função comum? R: É possível usar React Hooks (como
useStateeuseEffect) dentro de um hook personalizado, mas não dentro de uma função comum. Portanto, você deve usar um hook personalizado apenas para “lógica reutilizável que requer Hooks”, enquanto uma função comum é suficiente para “lógica computacional pura”. Por exemplo,formatDate()usa uma função comum, enquantouseWindowSize()usa um hook personalizado.
P: Os parâmetros de um hook personalizado podem ser funções? O valor de retorno pode ser JSX? R: Os parâmetros podem ser funções (por exemplo,
useKeyPress('Escape', callback)). O valor de retorno não pode ser JSX — o JSX deve ser retornado por componentes, não por hooks. Hooks retornam apenas dados ou funções. Se você precisar de “tanto a lógica de estado quanto a interface do usuário”, deve implementá-la como um componente ou usar Render Props.
P: Como se testa hooks personalizados? R: Use a biblioteca
@testing-library/react-hooks. Testar hooks personalizados é igual a testar um componente — renderize o hook, verifique o valor de retorno, acione uma atualização e verifique as alterações. Para hooks puramente lógicos, o teste é muito simples.
P: Quando se deve extrair a lógica para um hook personalizado? R: Quando você perceber que a mesma lógica aparece em dois ou mais componentes. Não a extraia muito cedo (antes de ter certeza de que a lógica realmente será reutilizada), mas também não espere demais (até ter copiado e colado cinco vezes). Uma regra prática útil: a “regra das três vezes” — se a mesma lógica aparecer três ou mais vezes, você deve extraí-la para um hook personalizado.
P: É possível usar
useEffectem um Hook personalizado? R: Sim, e isso é muito comum. Os Hooks personalizados podem usar qualquer Hook embutido (comouseState,useEffect,useRef,useContext, etc.). Por exemplo, o hookuseFetchusauseStateinternamente para gerenciar dados, carregamento e estados de erro;useEffectpara buscar dados na montagem; euseRefpara armazenar oAbortController. Esse é exatamente o valor dos hooks personalizados: combinar vários hooks em uma única unidade de lógica reutilizável.
📖 Resumo
- Um gancho personalizado é uma função que começa com
use; qualquer gancho embutido pode ser usado dentro dela. - O valor de retorno pode ser um array (como
useState), um objeto (vários valores de retorno) ou um único valor - 6 ganchos práticos: useToggle, useDebounce, useWindowSize, useFetch, useIntersectionObserver, useMediaQuery
- Siga a “Regra dos Três” — se um trecho de código for reutilizado, logicamente, três ou mais vezes, extraia-o para um hook
- Ganchos pequenos podem ser combinados para formar ganchos maiores, seguindo o Princípio da Responsabilidade Única
📝 Exercícios
- Problema básico (Dificuldade ⭐): Crie um hook
useCounterque retorne{ count, increment, decrement, reset }, com um valor inicial configurável. - Problema avançado (Dificuldade ⭐⭐): Crie um hook
useClipboardque encapsule a APInavigator.clipboarde retorne{ copied, copy(text) }, com suporte para copiar texto para a área de transferência. - Desafio (Dificuldade: ⭐⭐⭐): Crie um hook
useWebSocketque encapsule uma conexão WebSocket e retorne{ data, isConnected, send(message), reconnect() }, com suporte à reconexão automática e à detecção de heartbeat.