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.
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.
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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”:
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.
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:
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-hooksdecide pelo nome o que é hook: na função que segueuse+ 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.
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>;
}# eslint 10.9.0 com eslint-plugin-react-hooks 7.1.1
npx eslint consultorio.jsx✖ 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:
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:
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.
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:
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.
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.
node --test fila.test.mjsAs 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:
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)}`);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:
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:
Previous render Next render
- useState useState
- 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:
function abrirPlantao() {
const { total } = useAtendimentos();
return total;
}
function Consultorio() {
return <button onClick={() => console.log(abrirPlantao())}>abrir plantao</button>;
}No segundo erro, ignore primeiro as linhas internas do React e siga o caminho
do seu código: onClick → abrirPlantao → useAtendimentos. 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.
Perguntas frequentes
Preciso instalar alguma biblioteca para criar um hook customizado?
Onde eu guardo o arquivo do hook no projeto?
Hook customizado funciona em componente de classe?
Um hook customizado pode devolver JSX?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- React — Reutilizando lógica com Hooks customizados — pt-br.react.dev
- React — Regras dos Hooks — pt-br.react.dev
- Testing Library — renderHook — testing-library.com


