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

useRef no React: acessar o DOM e guardar valor

Como pegar um elemento com ref, dar foco num input, guardar valor entre renders sem provocar renderização e a diferença para o useState.

Rodolfo Mori15 min de leitura

A recepcionista clica em “Buscar”, mas o cursor não entra no campo. Depois, clica em “Parar” e o cronômetro continua correndo. Os dois bugs parecem diferentes, mas nascem da mesma dúvida: onde guardar algo que precisa sobreviver entre execuções do componente sem aparecer na tela?

React é uma biblioteca JavaScript para criar interfaces. Um componente é uma função que descreve uma parte da tela em JSX, marcação parecida com HTML; cada execução dessa função é um render. Estado é a memória que avisa ao React quando a interface precisa ser redesenhada. Um hook é uma função do React, como useRef e useState, que adiciona recursos ao componente.

useRef cria uma ref, uma referência persistente cujo valor fica em current. Ela pode apontar para um elemento do DOM — a representação da página no navegador — ou guardar um dado de bastidor. Nesta lição você vai escolher entre ref e estado, focar um campo, controlar um timer e diagnosticar o erro de current ainda vazio.

Os exemplos formam uma só aplicação: a recepção de uma clínica veterinária, com a busca de paciente, a ficha do Rex e o cronômetro da consulta.

Para que serve useRef no React?

Use useRef para guardar um nó do DOM — o objeto que representa um elemento da página — ou um valor auxiliar que atravessa renders sem redesenhar a tela. Se a pessoa precisa enxergar a mudança, esse dado deve ficar no estado.

Pense no componente como um balcão. O que o cliente precisa ver fica sobre ele e deve atualizar a placa da tela; isso é estado. Algumas coisas precisam apenas continuar guardadas nos bastidores — o identificador de um timer, o valor anterior, a referência para o campo de busca. useRef cria essa gaveta persistente. O objeto ref é a gaveta, e current é seu único compartimento.

A analogia ajuda a decidir onde guardar o dado, mas termina aí: uma gaveta real não conhece montagem, render nem a pureza do React — calcular a tela sem alterar dados externos. No código, trocar current não avisa a tela, e ler ou escrever essa propriedade durante o render não é permitido, salvo uma inicialização previsível mostrada mais adiante.

O que useRef devolve e quando current recebe o elemento do DOM?

useRef(null) devolve um objeto como { current: null }. Quando você o entrega a um elemento pelo atributo ref, o React preenche current após a montagem — a inserção no DOM. Durante o primeiro render, null representa ausência de valor.

jsx
import { useRef } from 'react';

export default function BuscaPaciente() {
  const campo = useRef(null);
  console.log('durante o render:', campo);

  function focarCampo() {
    console.log('no clique       :', campo.current.tagName, '/', campo.current.placeholder);
    campo.current.focus();
  }

  return (
    <form>
      <input ref={campo} placeholder="Nome do paciente" />
      <button type="button" onClick={focarCampo}>
        Buscar
      </button>
    </form>
  );
}

Experimente você mesmo

Antes de executar, preveja o conteúdo de campo.current durante o render e no clique. Depois monte o componente, clique em “Buscar” e confirme também se o campo recebe foco.

durante o render: { current: null } no clique : INPUT / Nome do paciente tem foco? : true

Três informações nessa saída. Primeira: durante o render, current ainda vale null — o <input> ainda nem existe na página. Segunda: no clique, current já é o elemento de verdade, com tagName e placeholder. Terceira, a linha que o teste imprimiu comparando document.activeElement com o input: focus() funcionou, e o cursor está piscando dentro do campo.

O console.log durante o render existe apenas para revelar a ordem do teste. Não use leitura de ref.current no corpo do componente como padrão de produção; leia e escreva refs em eventos — interações como clique — ou efeitos, tarefas executadas depois que o React sincroniza a tela. A exceção é a inicialização pura e previsível com current === null, explicada adiante.

Quem passa a ref para o React é você, no atributo ref={campo}. Quem preenche o current é o React, depois de colocar o elemento na tela. É a mesma ideia do querySelector e getElementById, com uma diferença que importa: querySelector procura no documento inteiro e pode achar o input de outro card da lista; o ref entrega o nó daquela instância do componente, sem precisar inventar um id único.

Até aqui: o React preenche a ref depois da montagem; por isso o clique já encontra o input, enquanto o primeiro render ainda encontra null.

Alterar ref.current causa um novo render no React?

Não. Alterar current preserva o valor, mas não agenda outro render. Se essa mudança precisa aparecer na interface, use estado; o comparativo abaixo torna a diferença observável.

São duas fichas de vacinação idênticas: uma guarda a contagem numa ref, a outra no estado. Ambas também contam quantas vezes o componente executou.

jsx
import { useRef } from 'react';

export default function FichaComRef() {
  const vacinas = useRef(0);
  const renders = useRef(0);
  renders.current += 1;

  function aplicar() {
    vacinas.current += 1;
  }

  function conferir() {
    console.log('ref   → vacinas:', vacinas.current, '| renders:', renders.current);
  }

  return (
    <div>
      <p>Vacinas aplicadas: {vacinas.current}</p>
      <button id="aplicar" onClick={aplicar}>Aplicar vacina</button>
      <button id="conferir" onClick={conferir}>Conferir no console</button>
    </div>
  );
}
jsx
import { useRef, useState } from 'react';

export default function FichaComState() {
  const [vacinas, setVacinas] = useState(0);
  const renders = useRef(0);
  renders.current += 1;

  function aplicar() {
    setVacinas((anterior) => anterior + 1);
  }

  function conferir() {
    console.log('state → vacinas:', vacinas, '| renders:', renders.current);
  }

  return (
    <div>
      <p>Vacinas aplicadas: {vacinas}</p>
      <button id="aplicar" onClick={aplicar}>Aplicar vacina</button>
      <button id="conferir" onClick={conferir}>Conferir no console</button>
    </div>
  );
}

Experimente você mesmo

Antes de clicar, preveja quatro valores após 50 aplicações: contagem guardada, texto na tela e número de renders em cada ficha. Depois faça os 50 cliques e use “Conferir no console” para comparar sua previsão com a saída.

ref → vacinas: 50 | renders: 1 na tela: Vacinas aplicadas: 0 state → vacinas: 50 | renders: 51 na tela: Vacinas aplicadas: 50 50 cliques com useRef : 0.8 ms 50 cliques com useState: 3.1 ms

Leia devagar, porque tem uma armadilha aí. As duas contagens chegaram a 50 — o ref guardou o valor tão bem quanto o estado. Mas a ficha com ref rodou um render, e a tela dela continua escrita “Vacinas aplicadas: 0”. O <p> foi desenhado uma vez, quando vacinas.current valia zero, e nunca mais.

A ficha com estado rodou 51 renders — o inicial mais um por clique — e a tela acompanhou.

Há instrumentação didática nos dois blocos: renders.current += 1 muda uma ref durante o render; no primeiro, o JSX também lê vacinas.current. Isso expõe a diferença no teste, mas não é padrão de produção. O render deve ser puro; em código real, acesse a ref em eventos e efeitos, e use estado para o que aparece.

clique em Aplicar vacina setVacinas(...) React roda o componente a tela mostra 50 clique em Aplicar vacina vacinas.current += 1 nenhum render a tela continua em 0

As duas últimas linhas da saída são o tempo dos 50 cliques, num MacBook com Node 24.16.0, média de 5 execuções descartando o aquecimento. O número exato oscila entre execuções — o useState ficou entre 3,1 ms e 6 ms nas medições, o useRef sempre abaixo de 2 ms. O que não oscila é a contagem de renders: 1 e 51, em toda execução.

Daí sai a regra prática: se o valor aparece na tela, ele é estado. Ref é para o que fica nos bastidores. E se você quiser entender o que exatamente faz o React rodar um componente de novo, é o assunto de re-render no React.

Até aqui: ref e estado preservam valores, mas só a atualização de estado avisa ao React que a interface precisa ser calculada novamente.

O valor passado ao useRef é calculado em todo render?

Sim: o JavaScript calcula a expressão passada a useRef a cada execução do componente, embora o React guarde apenas o primeiro resultado. Para construir um objeto puro e caro uma só vez, inicialize current sob uma condição segura.

A ficha abaixo guarda o número de consultas no estado e, ao lado, o primeiro número recebido pela ref:

jsx
import { useRef, useState } from 'react';

let leituras = 0;

function lerTabelaDeVacinas() {
  leituras += 1;
  return { antirrabica: 90, v10: 45, giardia: 60 };
}

export default function Ficha() {
  const [consultas, setConsultas] = useState(0);
  const tabela = useRef(lerTabelaDeVacinas());
  const primeiroValor = useRef(consultas);

  return (
    <div>
      <p>
        Consultas: {consultas} — primeiro valor guardado: {primeiroValor.current}
        antirrábica em {tabela.current.antirrabica} dias
      </p>
      <button onClick={() => setConsultas((n) => n + 1)}>Registrar consulta</button>
    </div>
  );
}

Três cliques em “Registrar consulta”:

Consultas: 0 — primeiro valor guardado: 0 — antirrábica em 90 dias Consultas: 1 — primeiro valor guardado: 0 — antirrábica em 90 dias Consultas: 2 — primeiro valor guardado: 0 — antirrábica em 90 dias Consultas: 3 — primeiro valor guardado: 0 — antirrábica em 90 dias lerTabelaDeVacinas rodou 4 vezes

primeiroValor.current ficou em 0 enquanto consultas subiu para 3. Isso é útil de propósito — é assim que se guarda “o valor que era” — mas pega gente desprevenida que esperava um espelho do estado.

O contador global leituras e as leituras de current no JSX são instrumentação para tornar o comportamento visível. Não copie essa parte para dados de interface: se o valor precisa aparecer, mantenha-o no estado.

A última linha é a parte cara. lerTabelaDeVacinas() rodou quatro vezes: uma na montagem e uma em cada um dos três renders que os cliques provocaram. O React só usou o resultado da primeira, e as outras três foram jogadas fora — o JavaScript precisa executar o argumento antes de passar. Diferente do useState, o useRef não tem forma preguiçosa para você entregar a função em vez do valor.

O jeito de contornar é inicializar dentro de um if:

jsx
import { useRef, useState } from 'react';

let leituras = 0;

function lerTabelaDeVacinas() {
  leituras += 1;
  return { antirrabica: 90, v10: 45, giardia: 60 };
}

export default function Ficha() {
  const [consultas, setConsultas] = useState(0);
  const tabela = useRef(null);
  if (tabela.current === null) {
    tabela.current = lerTabelaDeVacinas();
  }

  return (
    <div>
      <p>
        Consultas: {consultas} — antirrábica em {tabela.current.antirrabica} dias
      </p>
      <button onClick={() => setConsultas((n) => n + 1)}>Registrar consulta</button>
    </div>
  );
}
Consultas: 3 — antirrábica em 90 dias lerTabelaDeVacinas rodou 1 vezes

Uma leitura só. O leituras += 1 continua sendo apenas o contador do teste; a operação real deve ser pura. Esse if (current === null) é a exceção permitida durante o render quando sempre produz o mesmo objeto previsível. Vale para montar um índice ou instanciar uma classe sem efeitos colaterais. Para useRef(0) ou useRef(null), use a forma direta.

Como guardar o valor anterior com useRef?

Uma ref pode registrar o último valor dentro de um efeito para ser consultado depois por outro efeito ou evento. Se o valor anterior participa do JSX, modele um estado que guarde os dois pesos; ler current no render não é seguro.

A balança da clínica registra o peso do Rex a cada consulta, e a tela precisa mostrar quanto ele variou desde a pesagem anterior. O bloco abaixo torna a ordem temporal observável: a ref recebe o peso dentro de um efeito, função executada depois que a tela foi desenhada.

Ele é uma demonstração da mecânica, não um modelo para copiar quando a comparação precisa aparecer na interface:

jsx
import { useEffect, useRef, useState } from 'react';

const PESAGENS = [8.2, 7.6, 7.9, 9.4];

export default function Balanca() {
  const [indice, setIndice] = useState(0);
  const peso = PESAGENS[indice];

  const pesoAnterior = useRef(null);
  useEffect(() => {
    pesoAnterior.current = peso;
  }, [peso]);

  const anterior = pesoAnterior.current;

  return (
    <div>
      <p>
        Rex: {peso} kg — anterior:{' '}
        {anterior === null
          ? 'primeira pesagem'
          : `${anterior} kg (${peso > anterior ? '+' : ''}${(peso - anterior).toFixed(1)} kg)`}
      </p>
      <button onClick={() => setIndice((i) => i + 1)}>Registrar próxima pesagem</button>
    </div>
  );
}
Rex: 8.2 kg — anterior: primeira pesagem Rex: 7.6 kg — anterior: 8.2 kg (-0.6 kg) Rex: 7.9 kg — anterior: 7.6 kg (+0.3 kg) Rex: 9.4 kg — anterior: 7.9 kg (+1.5 kg)

A ordem é o segredo. Quando o peso vira 7,6, o componente roda com pesoAnterior.current ainda valendo 8,2 — a tela é desenhada com a comparação certa — e só depois o useEffect grava o novo valor, sem disparar render nenhum.

Esse resultado explica o tempo do efeito, mas o exemplo lê pesoAnterior.current durante o render. Pela regra de pureza do React, use a ref para comparações feitas em efeitos ou eventos; se a comparação é visível, guarde o peso atual e o anterior juntos no estado.

Também não basta trocar a ref por um useState gravado no mesmo efeito. Nesse arranjo específico, as quatro pesagens levam o contador de renders a 2, 4, 6 e 8. No segundo render o anterior já foi sobrescrito pelo peso atual, e a última linha estaciona em anterior: 9.4 kg (0.0 kg). A variação some para sempre. O ref não sofre disso porque gravar nele não provoca o segundo render.

O ponto é separar duas necessidades: bastidor consultado fora do render pode usar ref; histórico que compõe a tela pertence ao modelo de estado.

Como guardar o id de setInterval entre renders?

Guarde o identificador do setInterval numa ref. Uma variável local volta ao valor inicial em cada render; a ref conserva o identificador para que clearInterval encontre e pare a repetição.

O cronômetro da consulta é o caso em que a diferença entre variável comum e ref vira um bug observável. setInterval agenda uma função repetida e devolve um id, o identificador desse agendamento. A versão ingênua o guarda numa variável local:

jsx
import { useState } from 'react';

export default function CronometroQuebrado() {
  const [segundos, setSegundos] = useState(0);
  let idDoTimer = null;

  function iniciar() {
    idDoTimer = setInterval(() => setSegundos((s) => s + 1), 1000);
  }

  function parar() {
    clearInterval(idDoTimer);
  }

  return (
    <div>
      <p>Consulta do Rex: {segundos}s</p>
      <button id="iniciar" onClick={iniciar}>Iniciar</button>
      <button id="parar" onClick={parar}>Parar</button>
    </div>
  );
}

Trocando let idDoTimer = null por um ref, e nada mais:

jsx
import { useRef, useState } from 'react';

export default function Cronometro() {
  const [segundos, setSegundos] = useState(0);
  const idDoTimer = useRef(null);

  function iniciar() {
    if (idDoTimer.current !== null) return;
    idDoTimer.current = setInterval(() => setSegundos((s) => s + 1), 1000);
  }

  function parar() {
    clearInterval(idDoTimer.current);
    idDoTimer.current = null;
  }

  return (
    <div>
      <p>Consulta do Rex: {segundos}s</p>
      <button id="iniciar" onClick={iniciar}>Iniciar</button>
      <button id="parar" onClick={parar}>Parar</button>
    </div>
  );
}

O teste foi o mesmo nos dois: clicar em “Iniciar”, esperar 3 segundos de relógio, clicar em “Parar” e esperar mais 3 segundos.

let | no clique em Parar : Consulta do Rex: 3s let | 3s depois de Parar : Consulta do Rex: 6s useRef | no clique em Parar : Consulta do Rex: 3s useRef | 3s depois de Parar : Consulta do Rex: 3s

O cronômetro da primeira versão continuou correndo depois do “Parar” e chegou a 6 segundos. O motivo: cada tick do intervalo chama setSegundos, que provoca um render, e cada render executa let idDoTimer = null de novo. Quando você clica em “Parar”, o botão está ligado ao parar do último render — aquele em que idDoTimer é null. E clearInterval(null) não faz nada, sem reclamar.

Na versão corrigida, a condição evita iniciar dois intervalos. iniciar guarda o id em current; parar entrega esse id a clearInterval e volta a ref para null, deixando o cronômetro pronto para um novo início.

O ref não sofre disso porque é sempre o mesmo objeto: o React devolve a mesma caixa em todo render, e idDoTimer.current continua apontando para o id que foi guardado lá atrás. Se você ainda não viu como setInterval e clearInterval funcionam por fora do React, a lição de setTimeout e setInterval cobre isso.

Até aqui: variável local recomeça em cada render; a mesma ref atravessa todos eles e permite cancelar o timer certo.

Quando usar useRef ou useState?

Use estado com useState quando uma mudança precisa aparecer na tela. Use ref quando apenas eventos, efeitos ou uma API — recurso oferecido pelo navegador, como foco ou timer — precisa do valor. Velocidade não é o critério principal.

Na tabela, AbortController é um objeto do navegador usado para cancelar uma operação assíncrona, aquela que termina depois; flag é um sinal interno, geralmente true ou false.

o que você quer guardar hook por quê
quantidade, texto, item selecionado — qualquer coisa que aparece na tela useState a tela só atualiza quando o React é avisado
o elemento do DOM para focar, medir ou rolar useRef o nó não é dado de tela, é ferramenta
id de setInterval, setTimeout ou de um AbortController useRef é bastidor puro; mudar não precisa redesenhar nada
o valor anterior de um estado, para comparar useRef guardar o antigo não pode provocar um render novo
se um efeito já rodou uma vez useRef flag de controle; a tela não muda por causa dela
valor calculado a partir de outro estado nenhum dos dois calcule direto no corpo do componente
dado que vem do servidor e é exibido useState precisa redesenhar quando chega

A linha do valor anterior exige o limite visto acima: ref funciona quando a comparação acontece num efeito ou evento. Se o histórico compõe o JSX, ele precisa fazer parte do estado da interface.

Como passar ref para um componente no React 19?

No React 19, receba ref como uma prop e repasse-a ao elemento HTML. Prop é uma configuração entregue ao componente; se ele ignorar essa prop, a ref do pai continua null. No React 18, essa passagem exige forwardRef.

Compare os dois campos de busca: o primeiro ignora a ref, o segundo a entrega ao <input>.

jsx
function CampoBuscaSurdo({ etiqueta }) {
  return (
    <label>
      {etiqueta}
      <input placeholder="Nome do paciente" />
    </label>
  );
}

function CampoBusca({ etiqueta, ref }) {
  return (
    <label>
      {etiqueta}
      <input ref={ref} placeholder="Nome do paciente" />
    </label>
  );
}

A recepção usa um ou outro, sem mudar mais nada:

jsx
import { useRef } from 'react';

export default function Recepcao({ Campo, rotulo }) {
  const campo = useRef(null);

  function focar() {
    console.log(rotulo, '→ campo.current:', campo.current === null ? 'null' : campo.current.tagName);
    campo.current?.focus();
  }

  return (
    <div>
      <Campo etiqueta="Paciente" ref={campo} />
      <button onClick={focar}>Focar a busca</button>
    </div>
  );
}
sem repassar → campo.current: null tem foco? false com ref na prop → campo.current: INPUT tem foco? true

Sem repassar, o current do pai fica null para sempre e nenhum aviso aparece no console — o React 19 aceita ref como prop qualquer, e uma prop que o componente não usa é simplesmente ignorada. O ?. é optional chaining: ele só chama focus() se current existir. Isso evita o erro, mas não corrige o repasse; o botão continua sem focar o campo.

Por que ref.current ainda está null?

current fica null antes da montagem ou enquanto um elemento condicional ainda não existe. Espere um evento ou efeito após a montagem, ou use uma ref de callback para agir quando o próprio React entregar o nó.

No primeiro erro, a chamada de foco está no corpo do componente:

jsx
import { useRef } from 'react';

export default function BuscaPaciente() {
  const campo = useRef(null);
  campo.current.focus();

  return <input ref={campo} placeholder="Nome do paciente" />;
}
file:///private/tmp/pet-clinica/r5.mjs:8 campo.current.focus(); ^

TypeError: Cannot read properties of null (reading ‘focus’) at BuscaPaciente (file:///private/tmp/pet-clinica/r5.mjs:8:17) at Object.react_stack_bottom_frame (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20) at renderWithHooks (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22) at updateFunctionComponent (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19) at beginWork (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)

Node.js v24.16.0

O render acontece antes de o elemento existir. É o React executando a sua função para descobrir o que desenhar; nesse instante não há input nenhum no documento, e current é o null que você mesmo passou.

A segunda forma do mesmo erro é mais sutil, porque o clique parece um lugar seguro. O campo só existe depois que a recepcionista abre a consulta:

jsx
import { useRef, useState } from 'react';

export default function Recepcao() {
  const [aberto, setAberto] = useState(false);
  const campo = useRef(null);

  function abrirEFocar() {
    setAberto(true);
    campo.current.focus();
  }

  return (
    <div>
      <button onClick={abrirEFocar}>Nova consulta</button>
      {aberto && <input ref={campo} placeholder="Nome do paciente" />}
    </div>
  );
}
TypeError: Cannot read properties of null (reading 'focus') at abrirEFocar (file:///private/tmp/pet-clinica/r6.mjs:11:19) at executeDispatch (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) at runWithFiberInDEV (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:871:30) at processDispatchQueue (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:19166:19)

setAberto(true) não desenha o input na linha seguinte: ele agenda um render. Na linha do focus() o campo ainda não nasceu, e current continua null.

A correção mais direta é uma função no lugar do objeto. Quando você passa uma função para ref, você cria uma ref de callback. O React chama essa função com o nó assim que ele entra na tela e, no React 19, usa o retorno como função de limpeza quando o elemento sai:

jsx
import { useState } from 'react';

export default function Recepcao() {
  const [aberto, setAberto] = useState(false);

  function guardarCampo(no) {
    console.log('campo montado  :', no.tagName);
    no.focus();
    return () => console.log('campo desmontado');
  }

  return (
    <div>
      <button id="abrir" onClick={() => setAberto(true)}>Nova consulta</button>
      <button id="fechar" onClick={() => setAberto(false)}>Cancelar</button>
      {aberto && <input ref={guardarCampo} placeholder="Nome do paciente" />}
    </div>
  );
}

Clicando em “Nova consulta” e depois em “Cancelar”:

campo montado : INPUT tem foco? : true campo desmontado

O foco entrou no momento certo, sem useRef nenhum, porque quem avisa que o elemento chegou é o próprio React. Quando o assunto é foco automático na montagem, um useEffect com a chamada dentro resolve igual — o que não resolve é chamar focus() no meio do render ou logo depois de pedir uma atualização de estado.

O que estudar depois de useRef no React?

Na ordem 14 da trilha, estude useMemo e useCallback. Eles guardam resultado de cálculo ou identidade de função para otimização; não substituem ref nem estado.

A trilha de React mostra a sequência completa, e o guia de React organiza os hooks por assunto.

  • react
  • useref
  • dom
  • hooks
  • foco

Perguntas frequentes

Posso ler ref.current durante o render?
Ler é possível, mas não é confiável: o React não redesenha quando o current muda, então a tela pode ficar mostrando um valor velho. A regra da documentação é ler e escrever o current dentro de manipuladores de evento e de efeitos, não no corpo do componente.
useRef substitui o querySelector dentro de um componente React?
Sim, e deve substituir. O querySelector busca no documento inteiro e encontra o elemento de qualquer componente que casar com o seletor. O ref entrega o nó daquela instância, sem depender de classe nem de id único.
Quantos useRef posso ter no mesmo componente?
Quantos precisar. Cada chamada cria uma caixa independente, e a ordem das chamadas precisa ser sempre a mesma — como em qualquer hook, nada de useRef dentro de if ou de laço.
forwardRef parou de funcionar no React 19?
Não parou, ficou desnecessário para componente de função: o ref chega como prop comum. O forwardRef continua compilando e está marcado como obsoleto, então código novo não precisa dele.
Dá para colocar um ref em qualquer componente?
Em tag HTML, sempre. Em componente seu, só se ele fizer alguma coisa com o ref que recebe — se ele ignorar a prop, o current do pai fica null para sempre e a chamada quebra na hora do clique.

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 — useRef — react.dev
  2. React — Manipulating the DOM with Refs — react.dev
  3. React 19 — ref as a prop — react.dev

Continue por aqui