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

Refs e o DOM

React constrói e atualiza o DOM para você. Você descreve como a tela deve se parecer, e os elementos reais são responsabilidade do React. Um pequeno número de tarefas ainda precisa do elemento em si: focar um input depois que um botão é clicado, fazer scroll de uma mensagem para a vista, medir a largura de uma caixa, reproduzir um vídeo. Nenhuma delas pode ser expressa como um pedaço de JSX, porque cada uma age sobre um nó em vez de descrever um. Uma ref é a válvula de escape para elas. Ela dá a um componente um jeito de manter uma referência a um elemento real do DOM e chamar métodos nele diretamente.

Anexando uma ref

Uma ref começa com o hook useRef, e você passa o resultado para um elemento através do atributo ref:

jsx
import { useRef } from 'react'

function SearchBar() {
  const inputRef = useRef(null)

  function handleFocus() {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </>
  )
}

useRef(null) retorna um objeto com uma única propriedade, current, contendo o que você passou como valor inicial. Você sempre passa um, e null é o ponto de partida convencional para uma ref de DOM, já que não há nó naquele momento.

Escrever ref={inputRef} é a instrução que React precisa. Quando React coloca aquele <input> na tela, ele define inputRef.current para o nó do DOM. A partir daí inputRef.current é o elemento input em si, com cada método e propriedade que o navegador fornece, então inputRef.current.focus() dentro do handler de clique move o cursor para o campo. React mantém a atribuição atualizada para você: se o elemento for removido da tela, React define current de volta para null.

Fazendo scroll de um nó para a vista

Focar é uma tarefa. Scroll é a outra que você vai encontrar cedo, geralmente em algo como um log de chat que deve pular para a mensagem mais recente. Essa tarefa roda depois do render, então ela faz par com useEffect:

jsx
import { useRef, useEffect } from 'react'

function MessageList({ messages }) {
  const bottomRef = useRef(null)

  useEffect(() => {
    bottomRef.current.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  return (
    <div>
      {messages.map(message => (
        <p key={message.id}>{message.text}</p>
      ))}
      <div ref={bottomRef} />
    </div>
  )
}

O <div> vazio no final existe apenas como algo para fazer scroll até, que é um truque comum e perfeitamente razoável. O effect roda quando messages muda, e naquele ponto React já colocou as novas mensagens na tela e apontou bottomRef.current para aquele div final.

Mudar uma ref nunca dispara um render

State e refs sobrevivem de um render para o próximo, e aí a semelhança termina. Chamar um state setter pede a React para renderizar o componente novamente com o novo valor. Atribuir a ref.current muda o valor silenciosamente, e a tela fica exatamente como estava.

Esse silêncio é o ponto todo. Uma ref segura algo que a UI não exibe: um nó do DOM, um ID de timer, uma flag que sua lógica de renderização nunca lê. Renderizar novamente quando um desses muda custaria um redesenho que não poderia mudar um pixel sequer.

Tem uma segunda consequência que vale manter em mente. Porque nada precisa ser agendado, o valor em ref.current pode ser lido no instante depois que você o atribui. Uma variável de state é fixa por todo um render e só muda no próximo, como State aborda. Uma ref se comporta como uma caixa mutável comum que você pode ler e escrever a qualquer momento.

Quando uma ref é o instinto errado

A linha divisória é se o valor acaba na tela. Texto em um cabeçalho, um número em um badge, se um painel está aberto: tudo isso pertence a state, porque a exibição tem que mudar quando o valor muda. Coloque isso em uma ref e a atualização passa despercebida: o valor muda, e a UI continua mostrando tudo que renderizou por último.

Acessar o DOM para mudar o que é exibido é o mesmo instinto um passo além. Definir node.textContent manualmente, ou alternar uma classe diretamente, parece funcionar, e então o próximo render desfaz, porque React re-aplica tudo que seu JSX descreve. Refs cobrem duas coisas: ler e comandar um nó, ou seja, focar nele, fazer scroll nele, medir, reproduzir, e reter valores que sobrevivem entre renders sem nunca serem exibidos. Para qualquer coisa que o usuário leia na tela, descreva em JSX e dirija com state.

O segundo uso de uma ref nada tem a ver com o DOM. Qualquer valor que tenha que sobreviver entre renders sem ser exibido pode viver em ref.current, o que torna uma ref a casa natural para tarefas administrativas como um ID de timer:

jsx
function Stopwatch() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef(null)

  function start() {
    if (intervalRef.current !== null) return
    intervalRef.current = setInterval(() => setSeconds(s => s + 1), 1000)
  }

  function stop() {
    clearInterval(intervalRef.current)
    intervalRef.current = null
  }

  useEffect(() => () => clearInterval(intervalRef.current), [])

  return (
    <>
      <p>{seconds}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  )
}

O ID que setInterval devolve é a única forma de parar o timer depois, então stop precisa ser capaz de encontrá-lo. Uma variável local desapareceria no próximo render, e state dispararia um re-render desnecessário toda vez que o timer começasse. intervalRef.current é a forma certa para o trabalho, e também funciona como a verificação "já está rodando?" em start. O effect no final é a regra de limpeza usual de Effects: o timer foi iniciado aqui, então algo precisa pará-lo se o componente sair da tela enquanto ele está rodando.

O mesmo padrão te dá o valor anterior de uma prop ou uma variável de state, empacotado aqui como um custom hook:

jsx
function usePrevious(value) {
  const ref = useRef(undefined)

  useEffect(() => {
    ref.current = value
  })

  return ref.current
}

O effect roda depois de cada render, então a ref é atualizada apenas uma vez que o render que usou o valor antigo terminou, e o próximo render lê o que veio antes. No primeiro render a hook retorna undefined. Note o useRef(undefined) explícito: useRef recebe seu valor inicial como argumento, e os tipos do React 19 esperam que esse argumento esteja presente, então passe undefined deliberadamente quando não há nada significativo com que começar.

Mais um caso cotidiano: um campo de formulário não controlado. Quando um valor só importa no momento do envio e nada na tela depende dele enquanto o usuário digita, uma ref lê direto do input sem um re-render por tecla pressionada, que o capítulo Forms mostra em detalhes.

Ser preciso sobre quando ref.current segura um nó explica a maioria das surpresas. React trabalha em duas fases. Durante a fase de render ele chama sua função de componente para produzir uma descrição da UI, e nenhum DOM existe para essa descrição ainda. Durante a fase de commit ele aplica as mudanças ao DOM real, e é aí que as refs são anexadas: React define current para o nó que acabou de criar ou manter, então roda useLayoutEffect, depois deixa o navegador pintar, depois roda useEffect. Quando uma ref é desanexada, seja porque o elemento saiu da tela ou porque a prop ref agora aponta para outro lugar, React define current de volta para null antes de anexar a nova.

Então a ordem é: effects e event handlers veem uma ref preenchida sempre que o elemento está na tela, e o corpo do render não. No primeiro render, inputRef.current ainda é o null que você passou a useRef enquanto a função roda. Em renders posteriores ele segura o nó do commit anterior, que descreve uma tela prestes a ser substituída.

Esse último ponto é por que ler uma ref durante o render é inseguro. React espera que rendering seja puro, e ele se reserva o direito de renderizar um componente sem fazer commit do resultado: Strict Mode renderiza duas vezes em desenvolvimento, um render concorrente pode ser descartado quando uma atualização de prioridade mais alta chega, e uma árvore suspensa pode ser retentada. Uma medida feita durante o render pode portanto descrever um layout que não corresponde mais, e uma escrita para ref.current durante o render pode acontecer duas vezes ou ser descartada inteiramente. Mantenha leituras e escritas em event handlers, effects, ou callback refs, onde React fez commit e o timing é definido.

Uma callback ref é a outra forma de anexar uma. Passe uma função para ref e React a chama com o nó no anexo:

jsx
<div
  ref={node => {
    const observer = new ResizeObserver(() => {
      // reagir ao elemento mudando de tamanho
    })
    observer.observe(node)
    return () => observer.disconnect()
  }}
/>

No React 19 uma callback ref pode retornar uma função de limpeza, e React a chama quando aquele nó é desanexado. Isso substitui a convenção mais antiga para qualquer callback que retorna limpeza, e deixa setup e teardown juntos do jeito que estão em um effect. Um callback que retorna nada ainda é invocado uma segunda vez com null no desanexo, que é a forma que você vai encontrar em código existente. Uma borda afiada: uma função arrow inline é uma função fresca a cada render, então React desanexa e re-anexa o nó cada vez. Quando isso importa, envolva o callback em useCallback para sua identidade ficar estável.

Porque ref chega como uma prop ordinária no React 19, um componente também pode aceitar uma e passá-la para qualquer elemento que deveria recebê-la:

jsx
function TextInput({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

Exponha isso com moderação. Entregar a um pai um nó real do DOM dentro do seu componente torna o nó parte da sua API pública, e qualquer refatoração da marcação por baixo pode quebrar o chamador.

Nota de versão

Antes do React 19, componentes de função não podiam receber uma prop ref de jeito nenhum. Passar uma através para um elemento dentro exigia envolver o componente em forwardRef. React 19 entrega ref a componentes de função como uma prop ordinária, então forwardRef é desnecessário em código novo e está deprecado. Muito tempo atrás, componentes de classe criavam refs com React.createRef() e as liam de this. O hook useRef cobre ambos os papéis em componentes de função.

JunoUma ref alcança o nó real do DOM Chame useRef(null), coloque o resultado em um elemento com ref={inputRef}, e React preenche inputRef.current com o nó real do DOM uma vez que está na tela. Aí você pode fazer coisas com ele, como inputRef.current.focus().

Recorra a uma ref quando você precisa focar, fazer scroll, ou medir algo. Qualquer coisa que a pessoa usando seu app leia na tela pertence a state.

JunoUma ref alcança o nó real do DOMuseRef te dá uma caixa com uma propriedade current que sobrevive a cada render, e escrever nela nunca agenda um. Isso cobre dois trabalhos: alcançar um nó do DOM para focar, fazer scroll, ou medir, e guardar valores que a UI nunca exibe, como um ID de timer ou o valor anterior de uma prop.

Se o valor aparece na tela, use state para que a tela realmente atualize.

JunoUma ref alcança o nó real do DOM Refs anexam durante commit, antes de useLayoutEffect e useEffect, e desanexam de volta a null, então effects e handlers veem um nó vivo enquanto o corpo do render vê o do commit anterior. É por isso que ler ou escrever ref.current durante o render é inseguro: renders devem ficar puros, e React pode descartar ou repetir um.

Callback refs no React 19 podem retornar uma função de limpeza, que emparelha setup com teardown, embora um callback inline re-anexe a cada render a menos que você o estabilize com useCallback.

Próximo: Hooks, a família mais ampla que useState, useEffect, e useRef pertencem.