Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Hook customizado no React: extrair lógica repetida

Como transformar lógica com estado em um hook próprio, o que a regra do prefixo use significa de verdade e quando o hook não é a resposta.

Rodolfo Mori17 min de leitura

Você copia a mesma busca para duas telas. Depois corrige o tempo de espera em uma delas, esquece a outra e o bug volta. O problema não está no campo: está na lógica com estado espalhada pelo projeto. Nesta lição, você vai colocar esse procedimento em um hook customizado, reutilizá-lo sem misturar os estados e saber quando uma função comum é a escolha mais simples.

React é uma biblioteca JavaScript para criar interfaces. Um componente é uma função que descreve uma parte da tela; cada execução dessa função é um render. Estado é a memória do componente que, ao mudar, pede ao React uma nova renderização. Um efeito é uma tarefa executada para sincronizar a tela com algo externo, como timer ou API. Um hook é uma função que conecta o componente a recursos do React, como estado e efeitos.

Um hook customizado é uma função JavaScript sua, com nome começando em use, que chama outros hooks por dentro. Ele reúne um procedimento reutilizável com estado, efeitos e funções. Os exemplos formam um sistema de clínica veterinária: busca de pacientes, fila de espera e atendimentos por consultório.

Por que repetir lógica de formulário vira um problema?

Porque cada cópia vira mais um lugar para corrigir, testar e manter. O resultado pode funcionar hoje e mesmo assim deixar uma armadilha para a próxima mudança.

Pense no hook como o modelo de ficha de uma clínica: o papel tem os mesmos campos e instruções, mas cada paciente recebe uma folha nova. Da mesma forma, o hook distribui o mesmo procedimento, enquanto cada chamada mantém seus próprios valores e efeitos. O paralelo termina aí: uma ficha de papel não tem render nem ordem de hooks; no React, essas regras continuam valendo dentro da função.

O hook customizado compartilha lógica com estado, não uma mesma instância de estado. Antes de extrair, porém, vale enxergar a repetição no código real.

A recepção tem dois campos de busca: um procura o paciente (o animal), outro procura o tutor. Os dois precisam esperar a pessoa parar de digitar antes de bater na API, senão cada tecla vira uma requisição.

Experimente você mesmo

Antes de executar, preveja quantas requisições aparecerão depois de digitar r, re, rex em um campo e a, an, ana no outro. Depois compare sua previsão com a saída e localize as linhas que tiveram de ser copiadas.

jsx
function BuscaPacientes() {
  const [termo, setTermo] = useState('');
  const [termoAtrasado, setTermoAtrasado] = useState('');

  useEffect(() => {
    const id = setTimeout(() => setTermoAtrasado(termo), 300);
    return () => clearTimeout(id);
  }, [termo]);

  useEffect(() => {
    if (termoAtrasado) console.log('GET /pacientes?nome=' + termoAtrasado);
  }, [termoAtrasado]);

  return <input aria-label="paciente" value={termo} onChange={(e) => setTermo(e.target.value)} />;
}

function BuscaTutores() {
  const [termo, setTermo] = useState('');
  const [termoAtrasado, setTermoAtrasado] = useState('');

  useEffect(() => {
    const id = setTimeout(() => setTermoAtrasado(termo), 300);
    return () => clearTimeout(id);
  }, [termo]);

  useEffect(() => {
    if (termoAtrasado) console.log('GET /tutores?nome=' + termoAtrasado);
  }, [termoAtrasado]);

  return <input aria-label="tutor" value={termo} onChange={(e) => setTermo(e.target.value)} />;
}

Digitando r, re, rex no primeiro campo e a, an, ana no segundo — seis teclas ao todo — a saída é esta:

GET /pacientes?nome=rex GET /tutores?nome=ana

Funciona: seis teclas produziram duas requisições, uma para cada busca. O problema não é o resultado; são as sete linhas copiadas. Se alguém trocar 300 por 500 apenas na busca de pacientes, as duas telas passam a seguir regras diferentes sem que isso fique evidente.

Repare no que se repetiu: um useState guarda a versão atrasada do termo, e um useEffect agenda o setTimeout e limpa o anterior. Como o procedimento depende de estado e efeito, uma função pura não basta.

Como criar o hook useTermoAtrasado?

Extraia estado e efeito para uma função que começa com use, transforme as diferenças em parâmetros e devolva apenas o valor de que a tela precisa.

Primeiro vem a menor versão útil: receber um valor, esperar e devolver sua versão atrasada. Depois ela volta aos dois componentes do caso real.

jsx
function useTermoAtrasado(valor, espera = 300) {
  const [atrasado, setAtrasado] = useState(valor);

  useEffect(() => {
    const id = setTimeout(() => setAtrasado(valor), espera);
    return () => clearTimeout(id);
  }, [valor, espera]);

  return atrasado;
}

Leia de fora para dentro: valor é o texto atual, espera define o atraso e atrasado é o resultado devolvido. Sempre que uma dependência muda, o efeito cancela o timer anterior e cria outro. A função não exige registro, classe ou biblioteca externa; ela combina hooks que o React já oferece.

Os dois componentes encolhem, e ganham de graça a possibilidade de esperar tempos diferentes:

jsx
function BuscaPacientes() {
  const [termo, setTermo] = useState('');
  const termoAtrasado = useTermoAtrasado(termo);

  useEffect(() => {
    if (termoAtrasado) console.log('GET /pacientes?nome=' + termoAtrasado);
  }, [termoAtrasado]);

  return <input aria-label="paciente" value={termo} onChange={(e) => setTermo(e.target.value)} />;
}

function BuscaTutores() {
  const [termo, setTermo] = useState('');
  const termoAtrasado = useTermoAtrasado(termo, 800);

  useEffect(() => {
    if (termoAtrasado) console.log('GET /tutores?nome=' + termoAtrasado);
  }, [termoAtrasado]);

  return <input aria-label="tutor" value={termo} onChange={(e) => setTermo(e.target.value)} />;
}

A mesma digitação de antes, agora com uma marca de tempo no meio para você ver os 800 ms do segundo campo:

GET /pacientes?nome=rex --- 400 ms depois --- GET /tutores?nome=ana

A busca de pacientes disparou antes dos 400 ms; a de tutores, depois. O parâmetro mudou o tempo sem duplicar o procedimento.

Até aqui: o hook concentra a regra repetida e recebe por parâmetro aquilo que varia. Ele reutiliza o procedimento, mas ainda não compartilha os dados.

Um hook customizado compartilha estado entre componentes?

Não. Cada chamada recebe seu próprio estado, como dois apartamentos construídos com a mesma planta, mas com chaves e moradores diferentes.

O limite da analogia importa: os apartamentos existem fora do projeto; já os estados são associados pelo React à posição de cada componente na árvore da interface. Chamar useTermoAtrasado duas vezes cria dois useState independentes.

Dá para provar com um contador de atendimentos e dois consultórios:

jsx
function useAtendimentos(inicial = 0) {
  const [total, setTotal] = useState(inicial);
  const registrar = () => setTotal((t) => t + 1);
  const zerar = () => setTotal(0);
  return { total, registrar, zerar };
}

function Consultorio({ nome }) {
  const { total, registrar } = useAtendimentos();
  return (
    <section>
      <h2>{nome}</h2>
      <p>{total}</p>
      <button onClick={registrar}>registrar em {nome}</button>
    </section>
  );
}

Renderizando <Consultorio nome="Sala 1" /> e <Consultorio nome="Sala 2" /> lado a lado, com três cliques na primeira sala e um na segunda:

Sala 1: 3 atendimentos Sala 2: 1 atendimentos

As duas linhas confirmam a previsão: a Sala 1 chegou a 3 e a Sala 2 ficou em 1. O hook compartilhou o código do contador, mas não o valor de total. Se a Sala 2 precisa enxergar o número da Sala 1, o hook sozinho não resolve.

A tentação, nessa hora, é subir a variável para o topo do arquivo do hook. Não funciona, e vale ver o motivo:

jsx
let totalDaClinica = 0;

function useAtendimentosCompartilhados() {
  const [, forcarRender] = useState(0);
  const registrar = () => {
    totalDaClinica += 1;
    forcarRender((n) => n + 1);
  };
  return { total: totalDaClinica, registrar };
}

Três cliques na Sala 1, e depois imprimindo as duas salas e a variável:

Sala 1: 3 Sala 2: 0 variável do módulo: 3

A variável do módulo chegou a 3, mas a Sala 2 continuou mostrando 0: mudar uma variável comum não pede ao React outro render. Para compartilhar esse dado, o estado pode morar num ancestral comum, num provider ou numa store externa; props e Context API são formas de transportá-lo até os consumidores. Contexto não cria nem guarda o estado sozinho.

O que um hook deve devolver: array, objeto ou um valor?

O React aceita os três formatos. Escolha pelo número de itens e por quem deve decidir seus nomes; o formato do retorno vira a forma de uso do hook.

Devolvendo array, quem chama nomeia como quiser — é por isso que o próprio useState faz assim:

jsx
function useAlternar(inicial = false) {
  const [ligado, setLigado] = useState(inicial);
  return [ligado, () => setLigado((v) => !v)];
}

Devolvendo objeto, os nomes vêm de dentro do hook, e quem chama pega só o que precisa:

jsx
function useFilaDeEspera(inicial = []) {
  const [fila, setFila] = useState(inicial);
  return {
    fila,
    proximo: fila[0] ?? null,
    chamar: () => setFila((f) => f.slice(1)),
    entrar: (paciente) => setFila((f) => [...f, paciente]),
  };
}

No painel da recepção os dois convivem bem. Repare que useAlternar é chamado duas vezes no mesmo componente, com nomes diferentes:

jsx
function PainelDaRecepcao() {
  const [raioXAberto, alternarRaioX] = useAlternar();
  const [salaVipAberta, alternarSalaVip] = useAlternar(true);
  const { fila, proximo, chamar } = useFilaDeEspera(['Rex', 'Mel', 'Thor']);

  return (
    <div>
      <p>raio-x: {raioXAberto ? 'aberto' : 'fechado'} / sala vip: {salaVipAberta ? 'aberta' : 'fechada'}</p>
      <p>proximo: {proximo ?? 'ninguem'} | fila: {fila.join(', ') || '(vazia)'}</p>
      <button onClick={alternarRaioX}>raio-x</button>
      <button onClick={alternarSalaVip}>vip</button>
      <button onClick={chamar}>chamar</button>
    </div>
  );
}

Um clique em “raio-x” e um em “chamar”:

inicio | raio-x: fechado / sala vip: aberta | proximo: Rex | fila: Rex, Mel, Thor depois | raio-x: aberto / sala vip: aberta | proximo: Mel | fila: Mel, Thor

Use como ponto de partida: até dois itens cujos nomes dependem do contexto podem formar um array. Três ou mais itens, ou nomes que carregam significado (carregando, erro, dados), costumam ficar mais legíveis em objeto. Um único resultado pode ser devolvido diretamente, como em useTermoAtrasado. Não é uma regra do React; se a leitura piorar, escolha o formato mais claro.

Por que um hook customizado precisa ter nome como useFila?

O nome deve começar com use seguido de letra maiúscula, como useFila. Esse padrão permite que lint, DevTools e quem lê reconheçam a função como hook e apliquem corretamente suas regras. O JavaScript consegue executar outro nome em alguns casos, mas isso não torna a função um hook customizado válido para as ferramentas do ecossistema. Um nome como userData não segue esse padrão.

O teste abaixo troca o nome de useAtendimentos por contarAtendimentos e deixa visível a diferença entre runtime — o programa em execução — e análise do código.

jsx
function contarAtendimentos(inicial = 0) {
  const [total, setTotal] = useState(inicial);
  return { total, registrar: () => setTotal((t) => t + 1) };
}

function Consultorio() {
  const { total, registrar } = contarAtendimentos();
  return (
    <section>
      <p>{total}</p>
      <button onClick={registrar}>registrar</button>
    </section>
  );
}

Dois cliques:

atendimentos: 2

Os dois cliques chegaram a 2, portanto esse caso isolado rodou. Em tempo de execução, o React acompanha quantos hooks foram chamados e em que ordem. O prefixo use não é sintaxe da linguagem JavaScript; é a convenção necessária para que as ferramentas reconheçam a função e protejam essa ordem:

  • o eslint-plugin-react-hooks decide pelo nome o que é hook: na função que segue use + letra maiúscula (ou tem nome de componente) ele cobra as regras dos hooks na hora da chamada. Com outro nome, ele mira no lugar errado;
  • o React DevTools agrupa os estados por hook usando o nome;
  • quem lê a chamada sabe, sem abrir o arquivo, que aquela linha não pode entrar dentro de um if.

O primeiro item pode ser observado. O arquivo põe lado a lado uma função de nome comum e outra com o prefixo, ambas chamadas depois de um return condicional — situação proibida porque muda a ordem de hooks entre renders.

jsx
import { useState } from 'react';

function contarAtendimentos(inicial = 0) {
  const [total, setTotal] = useState(inicial);
  return { total, registrar: () => setTotal((t) => t + 1) };
}

function useAtendimentos(inicial = 0) {
  const [total, setTotal] = useState(inicial);
  return { total, registrar: () => setTotal((t) => t + 1) };
}

export function ConsultorioSemPrefixo({ plantaoAberto }) {
  if (!plantaoAberto) return <p>fechado</p>;
  const { total } = contarAtendimentos();
  return <p>{total}</p>;
}

export function ConsultorioComPrefixo({ plantaoAberto }) {
  if (!plantaoAberto) return <p>fechado</p>;
  const { total } = useAtendimentos();
  return <p>{total}</p>;
}
bash
# eslint 10.9.0 com eslint-plugin-react-hooks 7.1.1
npx eslint consultorio.jsx
/private/tmp/clinica/consultorio.jsx 4:29 error React Hook "useState" is called in function "contarAtendimentos" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks 21:21 error React Hook "useAtendimentos" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks

✖ 2 problems (2 errors, 0 warnings)

Leia primeiro os números das linhas. Sem o prefixo, o lint aponta a linha 4: useState apareceu numa função que ele não reconhece como hook. Ele não sinaliza a chamada condicional da linha 15. Com o prefixo, aponta a linha 21, onde o hook foi chamado depois do retorno. Tirar use não remove o problema; apenas impede a ferramenta de classificá-lo no lugar certo.

O terceiro item também não é estético. A seção de erros mostra o programa quebrando quando a chamada muda de posição entre renders.

Até aqui: começar com use não dá poderes mágicos à função; dá um nome reconhecível a uma função que deve obedecer às regras dos hooks.

Um hook customizado pode chamar outro hook?

Sim. Essa composição permite construir uma lógica maior a partir de hooks menores, sem copiar seus detalhes em cada componente.

Pense em peças de montar: useTermoAtrasado resolve a espera, enquanto useBuscaDePacientes encaixa essa peça na busca. O limite do paralelo é que os hooks usados aqui não podem ser encaixados em qualquer momento; as chamadas precisam continuar no topo do hook, na mesma ordem em cada render.

O useBuscaDePacientes junta o atraso da digitação com o estado de carregamento e o resultado da API, e devolve o pacote pronto:

jsx
function useBuscaDePacientes(buscar) {
  const [termo, setTermo] = useState('');
  const [resultados, setResultados] = useState([]);
  const [carregando, setCarregando] = useState(false);
  const termoAtrasado = useTermoAtrasado(termo);

  useEffect(() => {
    if (!termoAtrasado) {
      setResultados([]);
      return;
    }
    let cancelado = false;
    setCarregando(true);
    buscar(termoAtrasado).then((lista) => {
      if (cancelado) return;
      setResultados(lista);
      setCarregando(false);
    });
    return () => {
      cancelado = true;
    };
  }, [termoAtrasado, buscar]);

  return { termo, setTermo, resultados, carregando };
}

Com uma API falsa que demora 100 ms e a mesma digitação r, re, rex, imprimindo a linha de resultado em quatro momentos:

t=0ms nada t=0ms nada t=320ms carregando... t=520ms Rex, Rexona

Leia a linha do tempo: o atraso fica pronto perto de 300 ms e é observado em 320 ms. A API falsa termina cerca de 100 ms depois, perto de 420 ms; o roteiro só consulta a tela novamente em 520 ms. Portanto, 520 ms é o instante da observação, não a duração da requisição. A flag cancelado na limpeza impede uma resposta antiga de sobrescrever a nova — o mesmo cuidado mostrado ao consumir API no React, agora escrito uma vez só, dentro do hook.

Este bloco demonstra apenas o caminho de sucesso. Em código de produção, ainda é preciso desligar carregando se o termo for apagado, tratar rejeição com catch/finally e receber uma referência estável de buscar — declarada fora do componente ou estabilizada quando necessário. Uma função buscar inline e nova a cada render muda a dependência do efeito e pode disparar buscas de novo.

Toda função devolvida por um hook precisa de useCallback?

Não. Use useCallback somente quando algum consumidor depende da identidade da função, como um filho memorizado ou outro hook que a recebe como dependência. Um onClick comum não exige referência estável.

Sem useCallback, o corpo do hook roda novamente e cria outro objeto função em cada render. Com ele, o React pode devolver a referência anterior enquanto as dependências não mudam.

Em um hook reutilizável, devolver funções estáveis pode facilitar a vida de consumidores futuros, mas ainda é uma decisão de contrato e medição, não uma obrigação para todo callback. Em projetos configurados com React Compiler, parte da memoização manual também pode ser feita pelo compilador.

Experimente você mesmo

Antes dos cliques, preveja quais comparações mostrarão true: registrar, criada diretamente, ou zerar, envolvida em useCallback. Depois clique duas vezes e confira se a identidade de cada função confirma sua previsão.

jsx
function useAtendimentos() {
  const [total, setTotal] = useState(0);
  const registrar = () => setTotal((t) => t + 1);
  const zerar = useCallback(() => setTotal(0), []);
  return { total, registrar, zerar };
}

let registrarAnterior = null;
let zerarAnterior = null;

function Consultorio() {
  const { total, registrar, zerar } = useAtendimentos();

  if (registrarAnterior !== null) {
    console.log(
      `render ${total}: registrar é o mesmo? ${registrar === registrarAnterior} | zerar é o mesmo? ${zerar === zerarAnterior}`
    );
  }
  registrarAnterior = registrar;
  zerarAnterior = zerar;

  return <button onClick={registrar}>registrar</button>;
}

Dois cliques, e a comparação com o render anterior:

render 1: registrar é o mesmo? false | zerar é o mesmo? true render 2: registrar é o mesmo? false | zerar é o mesmo? true

As duas linhas repetem o padrão: registrar é outra referência; zerar continua igual. Enquanto a função serve apenas como onClick, estabilizá-la não traz benefício observável. Isso passa a importar quando ela chega a um filho envolvido em React.memo ou entra nas dependências de outro hook. Nesse caso, useCallback pertence ao hook que cria a função, evitando que cada consumidor tenha de corrigir sua identidade.

Como testar um hook com renderHook?

Use renderHook quando observar o hook isoladamente deixa o teste mais claro, leia o retorno em result.current e envolva em act as atualizações disparadas diretamente pelo teste. Embora você não monte um componente da sua tela, renderHook monta internamente um pequeno componente-hospedeiro, porque hooks só funcionam durante a renderização de componentes. Para comportamento de uma aplicação, renderizar um componente de teste costuma representar melhor o que o usuário realmente faz.

Extrair a lógica permite testá-la sem carregar a interface inteira da clínica. O teste a seguir observa fila, próximo paciente e ações do hook.

js
import './dom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { useState } from 'react';
import { renderHook, act } from '@testing-library/react';

function useFilaDeEspera(inicial = []) {
  const [fila, setFila] = useState(inicial);
  return {
    fila,
    proximo: fila[0] ?? null,
    chamar: () => setFila((f) => f.slice(1)),
    entrar: (paciente) => setFila((f) => [...f, paciente]),
  };
}

test('começa vazia e sem próximo', () => {
  const { result } = renderHook(() => useFilaDeEspera());
  assert.deepEqual(result.current.fila, []);
  assert.equal(result.current.proximo, null);
});

test('entrar coloca o paciente no fim da fila', () => {
  const { result } = renderHook(() => useFilaDeEspera(['Rex']));
  act(() => result.current.entrar('Mel'));
  assert.deepEqual(result.current.fila, ['Rex', 'Mel']);
  assert.equal(result.current.proximo, 'Rex');
});

test('chamar tira o primeiro e promove o seguinte', () => {
  const { result } = renderHook(() => useFilaDeEspera(['Rex', 'Mel', 'Thor']));
  act(() => result.current.chamar());
  assert.equal(result.current.proximo, 'Mel');
  assert.deepEqual(result.current.fila, ['Mel', 'Thor']);
});

test('chamar na fila vazia não quebra', () => {
  const { result } = renderHook(() => useFilaDeEspera());
  act(() => result.current.chamar());
  assert.equal(result.current.proximo, null);
});

O ./dom.mjs da primeira linha é o arquivo que cria um window de mentira com jsdom, porque o Node não tem DOM. Num projeto com Vitest você troca essa linha pela opção environment: 'jsdom' na configuração.

bash
node --test fila.test.mjs
✔ começa vazia e sem próximo (4.551541ms) ✔ entrar coloca o paciente no fim da fila (0.862791ms) ✔ chamar tira o primeiro e promove o seguinte (0.52225ms) ✔ chamar na fila vazia não quebra (0.531ms) ℹ tests 4 ℹ suites 0 ℹ pass 4 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 331.054625

As quatro linhas com confirmam os cenários; pass 4 e fail 0 resumem o resultado. Aqui, as ações chamadas diretamente por result.current ficam dentro de act, que conclui a atualização antes da próxima leitura. Sem ele no segundo teste, result.current.fila ainda pode aparecer como ['Rex'], e o React também avisa que a atualização não foi embrulhada em act(...). Eventos e utilitários do Testing Library que já usam act não precisam de outro embrulho manual.

Como este arquivo usa node:test, ele não recebe automaticamente o afterEach que alguns runners oferecem ao Testing Library. Numa suíte longa, chame cleanup() ou o unmount() devolvido por renderHook depois de cada cenário. O processo curto deste exemplo termina logo após os quatro testes, mas a omissão não deve virar padrão.

Até aqui: renderHook não executa o hook no vazio. Ele fornece o componente-hospedeiro, e act sincroniza as mudanças que o teste provoca.

Quando usar uma função pura em vez de um hook?

Use uma função pura quando o código apenas transforma valores e não chama hooks. Assim ele continua independente do React e pode rodar na tela, no servidor ou em um teste simples.

Uma função pura produz o mesmo resultado para as mesmas entradas e não altera nada fora dela. Calcular a idade do paciente a partir da data de nascimento é um exemplo:

js
function idadeDoPaciente(nascimento, hoje = new Date('2026-07-18')) {
  const meses =
    (hoje.getFullYear() - nascimento.getFullYear()) * 12 + (hoje.getMonth() - nascimento.getMonth());
  if (meses < 12) return `${meses} ${meses === 1 ? 'mês' : 'meses'}`;
  const anos = Math.floor(meses / 12);
  return `${anos} ${anos === 1 ? 'ano' : 'anos'}`;
}

const pacientes = [
  { nome: 'Rex', nascimento: new Date('2019-03-10') },
  { nome: 'Mel', nascimento: new Date('2026-01-15') },
  { nome: 'Thor', nascimento: new Date('2025-05-02') },
];

for (const p of pacientes) console.log(`${p.nome}: ${idadeDoPaciente(p.nascimento)}`);
Rex: 7 anos Mel: 6 meses Thor: 1 ano

Essa saída veio de node arquivo.mjs, sem React, jsdom ou componente. A função pode rodar dentro de map, if, teste ou servidor. Renomeá-la para useIdadeDoPaciente não cria vantagem; se ela passasse a chamar hooks, então ficaria sujeita às regras e ao ciclo de render do React.

a lógica que você quer reaproveitar vira por quê
chama useState, useEffect, useRef ou outro hook hook (useAlgo) depende da ordem de chamada dentro do render
só transforma valores que você já tem em mãos função pura, num arquivo qualquer roda em qualquer lugar, e é trivial de testar
devolve JSX componente tela é trabalho de componente, não de hook
conversa com a API e não guarda nada da tela função async comum, chamada de dentro de um hook separa a rede do estado, e você troca uma sem mexer na outra

Por que chamar um hook customizado condicionalmente quebra o componente?

Porque hooks como useState, useEffect e os hooks customizados deste artigo dependem da ordem das chamadas. Se um deles aparece em um render e some no seguinte, as posições deixam de corresponder.

Existe uma exceção avançada no React 19: a API chamada exatamente use pode aparecer em condições e loops. Essa permissão não vale para useState, useEffect nem para um hook customizado como useAtendimentos.

O plantão da clínica só abre à noite, então pode parecer razoável contar atendimentos apenas quando ele estiver aberto:

jsx
function Consultorio() {
  const [plantaoAberto, setPlantaoAberto] = useState(false);

  if (!plantaoAberto) {
    return <button onClick={() => setPlantaoAberto(true)}>abrir plantao</button>;
  }

  const { total, registrar } = contarAtendimentos();
  return <button onClick={registrar}>atendimentos: {total}</button>;
}

O primeiro render chama um hook. Clicando em “abrir plantao”, o segundo render chama dois:

React has detected a change in the order of Hooks called by Consultorio. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks

Previous render Next render

  1. useState useState
  2. undefined useState ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7848 throw Error(“Rendered more hooks than during the previous render.”); ^

Error: Rendered more hooks than during the previous render. at updateWorkInProgressHook (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7848:17) at updateReducer (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7967:18) at Object.useState (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:26511:18) at process.env.NODE_ENV.exports.useState (/private/tmp/clinica/node_modules/react/cjs/react.development.js:1263:34) at contarAtendimentos (file:///private/tmp/clinica/_07-erro-condicional.mjs:5:29) at Consultorio (file:///private/tmp/clinica/_07-erro-condicional.mjs:13:32) at Object.react_stack_bottom_frame (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20) at renderWithHooks (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22) at updateFunctionComponent (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19) at beginWork (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)

Node.js v24.16.0

A tabela do erro entrega a causa: no render anterior, a posição 2 era undefined; no seguinte, virou useState. Como contarAtendimentos não começa com use, o ESLint reclamou da declaração, mas não classificou esta chamada condicional no local correto. A correção é chamar o hook antes do return e condicionar apenas o JSX que aparece na tela. Se essa mensagem já apareceu no seu projeto, veja o diagnóstico completo em Rendered more hooks than during the previous render.

O primo desse erro é chamar o hook de dentro de uma função comum, como um handler de clique:

jsx
function abrirPlantao() {
  const { total } = useAtendimentos();
  return total;
}

function Consultorio() {
  return <button onClick={() => console.log(abrirPlantao())}>abrir plantao</button>;
}
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem. at Object.throwInvalidHookError (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7597:13) at process.env.NODE_ENV.exports.useState (/private/tmp/clinica/node_modules/react/cjs/react.development.js:1263:34) at useAtendimentos (file:///private/tmp/clinica/_11-fora-do-componente.mjs:5:29) at abrirPlantao (file:///private/tmp/clinica/_11-fora-do-componente.mjs:9:21) at onClick (file:///private/tmp/clinica/_11-fora-do-componente.mjs:13:85) at executeDispatch (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) at runWithFiberInDEV (/private/tmp/clinica/node_modules/react-dom/cjs/react-dom-client.development.js:871:30)

No segundo erro, ignore primeiro as linhas internas do React e siga o caminho do seu código: onClickabrirPlantaouseAtendimentos. O hook foi chamado depois do render, quando o React já não consegue associar o estado a um componente. A correção é chamar useAtendimentos no corpo de Consultorio e deixar o evento usar apenas os valores e funções já devolvidos.

O que estudar depois de hooks customizados?

Estude Context API para transportar até componentes distantes um estado mantido num ancestral ou provider. O contexto leva o valor; useState, useReducer ou outra fonte é que o guarda. O hook customizado reutiliza o procedimento, mas cada chamada continua isolada.

Antes de extrair, pergunte: “isso chama hooks?” e “a extração deixa o componente mais claro?”. O segundo uso é um bom sinal de repetição, não uma lei. Uma regra complexa pode merecer um hook já no primeiro componente; uma lógica de duas linhas pode continuar local mesmo quando se repete.

Na ordem 16 da trilha de React, a próxima lição mostra como a Context API transporta o valor até partes distantes da interface sem passar prop de andar em andar. O guia completo de React situa esse assunto no restante dos estudos.

  • react
  • hooks
  • hook customizado
  • reutilizacao

Perguntas frequentes

Preciso instalar alguma biblioteca para criar um hook customizado?
Não. Hook customizado não é recurso de biblioteca nenhuma: é uma função sua, num arquivo seu, que por dentro chama os hooks que o próprio React já exporta. Não existe import especial nem registro em lugar algum.
Onde eu guardo o arquivo do hook no projeto?
A convenção mais comum é uma pasta src/hooks com um arquivo por hook, nomeado igual à função (useFilaDeEspera.js). Se o hook só serve a um componente, deixe no mesmo arquivo dele até aparecer o segundo uso.
Hook customizado funciona em componente de classe?
Não. Hooks só existem dentro de componentes de função. Em código antigo com classes, a saída é envolver a classe num componente de função que chama o hook e repassa o resultado por props.
Um hook customizado pode devolver JSX?
Pode, tecnicamente, mas isso é sinal de que você queria um componente. Hook devolve dado e função; componente devolve tela. Misturar os dois tira do time a liberdade de renderizar o mesmo dado de outro jeito.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Reutilizando lógica com Hooks customizados — pt-br.react.dev
  2. React — Regras dos Hooks — pt-br.react.dev
  3. Testing Library — renderHook — testing-library.com

Continue por aqui