React: Ganchos personalizados

Última atualização: 2026-08-26

1. O que você vai aprender



2. A história de um requisito de reutilização de formulários

Padrões de reutilização para ganchos personalizados

100%
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:

JSX
// 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!)
}
▶ Experimente

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

JSX
// ---- 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
}
▶ Experimente

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

JSX
// ✅ 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() { }
▶ Experimente

(2) Projeto do valor de retorno

JSX
// 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
}
▶ Experimente

(3) Qualquer hook pode ser usado internamente

JSX
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
▶ Experimente

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)

JSX
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>
  )
}
▶ Experimente

(2) Hook 2: useDebounce (anti-jitter)

JSX
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..." />
}
▶ Experimente

(3) Hook 3: useWindowSize (tamanho da janela)

JSX
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>
  )
}
▶ Experimente

(4) Hook 4: useFetch (solicitação de dados)

JSX
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>
}
▶ Experimente

(5) Hook 5: useIntersectionObserver (Detecção de visibilidade)

JSX
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>
  )
}
▶ Experimente

(6) Hook 6: useMediaQuery (Pontos de quebra responsivos)

JSX
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>
  )
}
▶ Experimente

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

JSX
// ============================================
// 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
▶ Experimente

6. Exemplo completo: Galeria de imagens (combinando vários ganchos personalizados)

JSX
// ============================================
// 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

JSX
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>
  )
}
▶ Experimente

▶ Exemplo 3: Hook useOnlineStatus — Detecção do status da rede

JSX
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>
  )
}
▶ Experimente

▶ Exemplo 4: Hook useLocalStorage — Persistência de estado

JSX
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>
  )
}
▶ Experimente

▶ Exemplo 5: Hook useCountdown — Contagem regressiva

JSX
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>
  )
}
▶ Experimente

❓ Perguntas Frequentes

P: Qual é a diferença entre um hook personalizado e uma função comum? R: É possível usar React Hooks (como useState e useEffect) 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, enquanto useWindowSize() 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 useEffect em um Hook personalizado? R: Sim, e isso é muito comum. Os Hooks personalizados podem usar qualquer Hook embutido (como useState, useEffect, useRef, useContext, etc.). Por exemplo, o hook useFetch usa useState internamente para gerenciar dados, carregamento e estados de erro; useEffect para buscar dados na montagem; e useRef para armazenar o AbortController. Esse é exatamente o valor dos hooks personalizados: combinar vários hooks em uma única unidade de lógica reutilizável.


📖 Resumo


📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Crie um hook useCounter que retorne { count, increment, decrement, reset }, com um valor inicial configurável.
  2. Problema avançado (Dificuldade ⭐⭐): Crie um hook useClipboard que encapsule a API navigator.clipboard e retorne { copied, copy(text) }, com suporte para copiar texto para a área de transferência.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um hook useWebSocket que encapsule uma conexão WebSocket e retorne { data, isConnected, send(message), reconnect() }, com suporte à reconexão automática e à detecção de heartbeat.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%