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 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 após um botão ser clicado, rolar 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 saída de emergência para elas. Ela dá ao 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 do DOM, já que não há nó naquele momento.

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

Rolando um nó para a vista

Foco é uma tarefa. Rolagem é a outra que você enfrentará cedo, geralmente em algo como um log de chat que deve pular para a mensagem mais recente. Essa tarefa roda após render, então se combina 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>
  )
}

A <div> vazia no final existe apenas como algo para rolar até, o que é um truque comum e perfeitamente razoável. O efeito roda sempre que messages muda, e nesse momento o React já colocou as novas mensagens na tela e apontou bottomRef.current para aquela div final.

Alterar uma ref nunca dispara um render

Estado e refs ambos sobrevivem de um render para o próximo, e aí a semelhança termina. Chamar um setter de estado pede ao 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 sentido todo. Uma ref mantém 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ê. Re-renderizar quando um desses mudar custaria uma redesenho que não poderia mudar um único pixel.

Tem uma segunda consequência que vale a pena reter. Como nada precisa ser agendado, o valor em ref.current é legível no instante depois que você o atribui. Uma variável de estado é fixa durante todo um render e só muda no próximo, como State cobre. Uma ref se comporta como uma caixa mutável comum que você pode ler e escrever em 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 distintivo, se um painel está aberto: tudo isso pertence a state, porque a exibição tem que mudar quando o valor muda. Coloque em uma ref e a atualização passa despercebida: o valor muda, e a UI continua mostrando o que renderizou por último.

Alcançar o DOM para mudar o que é exibido é o mesmo instinto um passo adiante. Definir node.textContent manualmente, ou alternar uma classe diretamente, parece funcionar, e então o próximo render desfaz, porque o React reaplica o que seu JSX descreve. Refs cobrem duas coisas: ler e comandar um nó, ou seja, focar nele, rolar, medir, reproduzir, e manter valores que sobrevivem através de renders sem nunca serem exibidos. Para qualquer coisa que o usuário leia da tela, descreva em JSX e conduza com state.

O segundo uso de uma ref não tem nada a ver com o DOM. Qualquer valor que tem que sobreviver através de renders sem ser exibido pode viver em ref.current, o que torna uma ref o lar natural para contabilidade 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 tem que conseguir encontrá-lo. Uma variável local seria perdida no próximo render, e state dispararia um re-render inútil toda vez que o timer começasse. intervalRef.current é a forma certa para o trabalho, e dobra como a verificação "já está rodando?" em start. O efeito no final é a regra de limpeza usual de Effects: o timer começou aqui, então algo tem que 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 variável de state, empacotado aqui como um hook customizado:

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

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

  return ref.current
}

O efeito roda após 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 dele. No primeiro render a função retorna undefined. Veja o useRef(undefined) explícito: useRef toma 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 para começar.

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

Ser preciso sobre quando ref.current mantém um nó explica a maioria das surpresas. React funciona 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 aquela descrição ainda. Durante a fase de commit ele aplica as mudanças ao DOM real, e é aí que refs são anexadas: o React define current para o nó que ele acabou de criar ou manteve, então roda useLayoutEffect, então deixa o navegador pintar, então roda useEffect. Quando uma ref é desanexada, tanto porque o elemento saiu da tela quanto porque a prop ref agora aponta para outro lugar, o React define current de volta para null antes de anexar a nova.

Então a ordem é: efeitos e handlers de evento 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 para useRef enquanto o corpo da função roda. Em renders posteriores ele mantém o nó do commit anterior, que descreve uma tela que está prestes a ser substituída.

Esse último ponto é por que ler uma ref durante render é inseguro. O React espera que renderização seja pura, 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 maior chega, e uma árvore suspensa pode ser tentada novamente. Uma medida feita durante render pode portanto descrever um layout que não combina mais, e uma escrita para ref.current durante render pode acontecer duas vezes ou ser descartada inteiramente. Mantenha leituras e escritas em handlers de eventos, efeitos, ou callback refs, onde o React fez commit e o timing é definido.

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

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 o React a chama quando aquele nó é desanexado. Isso substitui a convenção antiga para qualquer callback que retorna uma limpeza, e deixa setup e teardown sentarem juntos do jeito que fazem em um efeito. Um callback que não retorna nada ainda é invocado uma segunda vez com null ao desanexar, que é a forma que você encontrará em código existente. Uma borda afiada: uma função arrow inline é uma função fresca em cada render, então o React desanexa e reanexaa o nó cada vez. Quando isso importa, envolva o callback em useCallback para que sua identidade fique estável.

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

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

Exponha isso com moderação. Dar a um pai um nó DOM vivo dentro do seu componente torna o nó parte da sua API pública, e qualquer refator da markup por debaixo pode quebrar o chamador.

Nota de versão

Antes do React 19, componentes de função não podiam receber uma prop ref em absoluto. Passar uma através para um elemento dentro exigia envolver o componente em forwardRef. O React 19 entrega ref para componentes de função como uma prop ordinária, então forwardRef é desnecessário em código novo e está deprecado. Mais atrás, componentes de classe criavam refs com React.createRef() e as liam de this. O hook useRef cobre ambas as funções 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 o React preenche inputRef.current com o nó DOM real uma vez que está na tela. Então você pode fazer coisas com ele, como inputRef.current.focus(). Recorra a uma ref quando você precisa focar, rolar, ou medir algo. Qualquer coisa que a pessoa usando seu app leia da tela pertence a state.
JunoUma ref alcança o nó real do DOMuseRef te dá uma caixa com uma propriedade current que sobrevive cada render, e escrever nela nunca agenda um. Isso cobre duas funções: alcançar um nó do DOM para foco, rolagem, ou medida, 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 para null, então efeitos e handlers veem um nó vivo enquanto o corpo do render vê o commit anterior. É por isso que ler ou escrever ref.current durante render é inseguro: renders têm que ficar puros, e o 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 reanexe cada render a menos que você o estabilize com useCallback.

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