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