Skip to content
This page has been auto-translated and may contain errors.View in English

Hooks personalizados na prática

O capítulo Hooks apresentou a ideia: um hook personalizado é uma função que começa com use e compõe hooks em lógica reutilizável. Este capítulo mostra como isso funciona na prática, e o curso faz o ponto de forma dramática: depois de toda uma seção construindo um componente Toggle sem renderização, um hook personalizado o substitui em uma fração do código, e o componente é deletado.

A linha que vale a pena desenhar com precisão é contra uma função utilitária comum. Uma função que busca dados ou formata uma data é uma utilidade. Um hook personalizado chama outros hooks, é por isso que as regras de hooks se aplicam dentro dele.

Extraindo um efeito: useEffectOnUpdate

O componente Toggle acabou com um pedaço de lógica cuja função era "executar este callback quando o estado muda, mas nunca na primeira renderização": um useEffect protegido por uma ref firstRender. Isso não é lógica de toggle; é lógica de efeito, útil em qualquer lugar. Extrair isso produz um hook com a mesma assinatura do próprio useEffect:

jsx
export default function useEffectOnUpdate(effectFunction, deps) {
  const firstRender = useRef(true)

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false
    } else {
      effectFunction()
    }
  }, deps)
}

Quem chama escreve useEffectOnUpdate(onToggle, [on]) e a contabilidade da primeira renderização desaparece do seu código. Hooks que envolvem outros hooks assim às vezes valem a pena construir mesmo quando a lógica envolvida tem apenas algumas linhas, porque o nome comunica a intenção.

A guarda tem uma complicação só em desenvolvimento

A mesma que render props sinalizou: StrictMode consome a ref na primeira execução do efeito, então o callback dispara na segunda. Builds de produção não fazem isso, e a correção durável é comparar o valor contra o anterior em vez de rastrear se esta é a primeira renderização.

Extraindo estado: useToggle

useEffectOnUpdate não retorna nada, como useEffect. A maioria dos hooks personalizados devolve algo, como useState faz, e a forma do que retornam é uma decisão de design. useToggle reúne o booleano, a função de alternância e o callback de mudança em um só lugar:

jsx
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
  const [on, setOn] = useState(initialValue)
  const toggle = () => setOn(prevOn => !prevOn)

  useEffectOnUpdate(onToggle, [on])

  return [on, toggle]
}
jsx
const [on, toggle] = useToggle({ initialValue: true })

Cada chamada a useToggle recebe seu próprio useState, então dois chamadores mantêm dois booleanos independentes, sejam dois componentes ou um componente chamando o hook duas vezes.

Três decisões ali, cada uma um padrão que você reutilizará:

  • Retorne um array quando os valores são um par natural que o chamador vai renomear, exatamente como useState: um chamador desestrutura [on, toggle], outro [open, toggleOpen]. Retorne um objeto quando há vários valores e os nomes importam mais que a ordem.
  • Receba um objeto de configuração em vez de parâmetros posicionais. Com useToggle(false, callback) um chamador que só quer o callback é obrigado a fornecer o booleano primeiro; com { initialValue, onToggle } ele passa o que quer dizer e pula o resto, e toda opção pode vir com um padrão.
  • Estabeleça o callback como noop padrão (() => {}) para que o hook possa chamar onToggle() incondicionalmente sem quebrar chamadores que nunca passaram um.

O resultado: deletando o componente

Com useToggle em mãos, o widget de estrela para de precisar de qualquer maquinaria de Toggle:

jsx
export default function Star({ onChange }) {
  const [on, toggle] = useToggle({ onToggle: onChange })

  return on
    ? <BsStarFill className="star filled" onClick={toggle} />
    : <BsStar className="star" onClick={toggle} />
}

O menu mantém sua forma de componente composto, porque suas peças ainda precisam de estado implícito compartilhado, mas seu provedor agora pega seus valores do hook: Menu chama useToggle, passa { open, toggleOpen } pelo seu próprio contexto, e os arquivos do componente Toggle sem renderização são deletados.

Esse apagamento é a verdadeira lição da seção. O componente sem renderização se justificou enquanto muitos componentes compostos poderiam compartilhá-lo; uma vez que a lógica cabia em um hook, o hook era a ferramenta mais simples, e remover código funcionando do qual você se orgulhava é uma parte normal do trabalho.

Os dois padrões se complementam: um hook compartilha lógica, um componente sem renderização compartilha lógica pela árvore via contexto e composição. Comece com o hook, e deixe um componente ganhar a cerimônia extra. O curso encerra essa seção com um projeto solo, Component Library++, que coloca todo padrão desses cinco capítulos em uma biblioteca.

Um hábito que vale a pena formar cedo: antes de escrever um hook personalizado, verifique se a comunidade já tem um. Valores com debounce, media queries, estado de armazenamento local, listeners de eventos, rastreamento de valor anterior, tudo isso existe como hooks comprovados em pequenas bibliotecas, e ler o código fonte delas é um jeito rápido de absorver os idiomas. Escrever o seu próprio ainda vence quando a lógica é específica da sua app, e hooks específicos da app como useCurrentUser ou useCartTotal são frequentemente a camada mais limpa em que a lógica de dados da app pode viver: componentes permanecem declarativos, e as partes bagunçadas ficam em uma função nomeada, testável.

Hooks personalizados compartilham lógica; eles não compartilham estado. Quando dois chamadores devem ver o mesmo valor, o hook sozinho não consegue fazer: o estado tem que viver uma vez, em um provedor, e o hook se torna o leitor, o padrão useTheme do capítulo de contexto. Manter "lógica reutilizável" e "estado compartilhado" distintos na sua cabeça evita o erro clássico de esperar que um hook funcione como uma store.

Dois detalhes decidem se hooks assim são agradáveis de enviar. O primeiro é identidade: useToggle constrói uma função toggle nova em cada renderização, então um chamador que a passa para um filho memoizado ou para um array de dependência de outro hook entrega um novo valor cada vez. Envolva em useCallback dentro do hook quando chamadores dependem de uma referência estável.

O segundo é ferramental. Encaminhar um array deps por um hook envolvedor funciona em tempo de execução enquanto seu comprimento for constante, mas exhaustive-deps não consegue verificar um array que não viu escrito, e ignora useEffectOnUpdate(onToggle, [on]) no local de chamada inteiramente a menos que o projeto liste o hook na opção additionalHooks da regra. Um codebase que se apoia em hooks envolvedores deve adicionar essa configuração, para que os avisos que o capítulo de efeitos te disse para tratar como bugs reais continuem chegando até você.

JunoColoque a lógica em garrafa, reutilize em qualquer lugar Um hook personalizado é uma função cujo nome começa com use e que se constrói sobre hooks como useState para empacotar um pedaço de lógica. Uma vez que useToggle existe, qualquer componente consegue um valor ligado-desligado e uma função de alternância em uma linha, do mesmo jeito que useState te entrega um valor e um setter.

Cada componente que o chama recebe sua própria cópia do estado, então um hook é uma receita que você reutiliza em vez de um valor que todos compartilham.

JunoColoque a lógica em garrafa, reutilize em qualquer lugar Extraia lógica com estado repetida em hooks: useEffectOnUpdate para efeitos que pulam a primeira renderização, useToggle para estado booleano com um callback onToggle.

Retorne um array quando chamadores vão renomear um par natural, receba um objeto de configuração com padrões em vez de parâmetros posicionais, e estabeleça callbacks como noop padrão.

Prefira um hook sobre um componente sem renderização até que a lógica precise viajar pela árvore.

JunoColoque a lógica em garrafa, reutilize em qualquer lugar Um hook personalizado compõe hooks, herda suas regras transitivamente, e compartilha lógica enquanto cada chamador mantém estado independente; combine com um provedor quando o estado deve ser compartilhado em vez de duplicado.

Dois detalhes decidem se um é agradável de enviar: as funções que retorna são reconstruídas em cada renderização, então envolva em useCallback quando chamadores as passam para filhos memoizados ou para arrays de dependência, e um hook envolvedor que encaminha um array deps permanece invisível a exhaustive-deps até que o projeto o nomeie na opção additionalHooks da regra.

Próximo: Roteamento, onde a URL começa a decidir quais componentes renderizam.