React & Frontend

O que Todo Dev Deve Saber sobre useRef Avançado: DOM, Valores Mutáveis e Comunicação entre Renders

13 min de leitura

O que Todo Dev Deve Saber sobre useRef Avançado: DOM, Valores Mutáveis e Comunicação entre Renders

O que é useRef e por que vai além de useState O é um hook do React que retorna um objeto mutável cuja propriedade persiste entre renders. Diferente de , modificar um ref não dispara re-renders. Isso torna o hook perfeito para casos onde você precisa manter valores sem afetar a renderização ou acessar elementos do DOM diretamente. A razão pela qual é tão poderoso é que ele quebra o ciclo de dados unidirecional do React. Enquanto força a filosofia "estado muda → componente re-renderiza", permite armazenar dados que podem ser lidos e escritos sem consequências de renderização. Isso é especialmente útil em cenários onde performance importa ou quando você precisa integrar com APIs imperativos do navegador. Vamos pensar em um caso prático: um aplicativo que precisa focar um input quando um botão é clicado, ou um contador que incrementa internamente sem atualizar a UI. Esses são cenários perfeitos para . Você verá que, apesar de simples em conceito, a

<h2>O que é useRef e por que vai além de useState</h2>

<p>O <code>useRef</code> é um hook do React que retorna um objeto mutável cuja propriedade <code>.current</code> persiste entre renders. Diferente de <code>useState</code>, modificar um ref não dispara re-renders. Isso torna o hook perfeito para casos onde você precisa manter valores sem afetar a renderização ou acessar elementos do DOM diretamente.</p>

<p>A razão pela qual <code>useRef</code> é tão poderoso é que ele quebra o ciclo de dados unidirecional do React. Enquanto <code>useState</code> força a filosofia &quot;estado muda → componente re-renderiza&quot;, <code>useRef</code> permite armazenar dados que podem ser lidos e escritos sem consequências de renderização. Isso é especialmente útil em cenários onde performance importa ou quando você precisa integrar com APIs imperativos do navegador.</p>

<p>Vamos pensar em um caso prático: um aplicativo que precisa focar um input quando um botão é clicado, ou um contador que incrementa internamente sem atualizar a UI. Esses são cenários perfeitos para <code>useRef</code>. Você verá que, apesar de simples em conceito, a aplicação correta do hook resolve problemas que seriam complicados com <code>useState</code>.</p>

<h2>Acessando e Manipulando o DOM Diretamente</h2>

<h3>Referências a elementos DOM</h3>

<p>A forma mais comum de usar <code>useRef</code> é obter uma referência direta a um elemento do DOM. Você anexa o ref ao atributo <code>ref</code> de um elemento JSX, e então acessa a instância real do DOM via <code>.current</code>.</p>

<pre><code class="language-javascript">import { useRef } from &#039;react&#039;;

function TextInputWithFocus() {

const inputRef = useRef(null);

const handleFocus = () =&gt; {

if (inputRef.current) {

inputRef.current.focus();

inputRef.current.style.borderColor = &#039;blue&#039;;

}

};

return (

&lt;&gt;

&lt;input ref={inputRef} type=&quot;text&quot; placeholder=&quot;Digite algo...&quot; /&gt;

&lt;button onClick={handleFocus}&gt;Focar no input&lt;/button&gt;

&lt;/&gt;

);

}

export default TextInputWithFocus;</code></pre>

<p>Aqui, <code>inputRef.current</code> aponta para o elemento <code>&lt;input&gt;</code> real do DOM. Quando você clica no botão, a função <code>handleFocus</code> chama o método <code>.focus()</code> diretamente no elemento, sem passar por nenhum sistema de estado. Isso é imperativo puro — você está dizendo ao navegador exatamente o que fazer.</p>

<h3>Integrando com bibliotecas externas</h3>

<p>Muitas bibliotecas JavaScript (como D3, Chart.js ou editor de código) funcionam de forma imperativa. Elas esperam um nó do DOM e controlam tudo internamente. <code>useRef</code> é a ponte perfeita entre o mundo declarativo do React e essas bibliotecas.</p>

<pre><code class="language-javascript">import { useRef, useEffect } from &#039;react&#039;;

import Chart from &#039;chart.js/auto&#039;;

function BarChart() {

const canvasRef = useRef(null);

const chartRef = useRef(null);

useEffect(() =&gt; {

if (canvasRef.current &amp;&amp; !chartRef.current) {

chartRef.current = new Chart(canvasRef.current, {

type: &#039;bar&#039;,

data: {

labels: [&#039;Jan&#039;, &#039;Fev&#039;, &#039;Mar&#039;, &#039;Abr&#039;],

datasets: [{

label: &#039;Vendas&#039;,

data: [12, 19, 3, 5],

backgroundColor: &#039;rgba(75, 192, 192, 0.2)&#039;,

borderColor: &#039;rgba(75, 192, 192, 1)&#039;,

borderWidth: 1

}]

}

});

}

return () =&gt; {

if (chartRef.current) {

chartRef.current.destroy();

}

};

}, []);

return &lt;canvas ref={canvasRef} width=&quot;400&quot; height=&quot;200&quot;&gt;&lt;/canvas&gt;;

}

export default BarChart;</code></pre>

<p>O padrão aqui é crucial: criamos a instância da biblioteca só uma vez (verificamos se já existe com <code>!chartRef.current</code>), armazenamos em um ref e limpamos quando o componente é desmontado. Assim evitamos múltiplas instâncias e vazamento de memória.</p>

<h2>Valores Mutáveis e Persistência Entre Renders</h2>

<h3>Armazenando valores sem disparo de re-renders</h3>

<p>Um uso avançado de <code>useRef</code> é manter valores que mudam, mas não precisam atualizar a interface. Imagine um timer que conta internamente enquanto você vê apenas o resultado final quando pressiona um botão. <code>useRef</code> permite isso sem criar renders desnecessários.</p>

<pre><code class="language-javascript">import { useRef, useState } from &#039;react&#039;;

function StopWatch() {

const [displayTime, setDisplayTime] = useState(0);

const timeRef = useRef(0);

const intervalRef = useRef(null);

const isRunningRef = useRef(false);

const handleStart = () =&gt; {

if (isRunningRef.current) return;

isRunningRef.current = true;

intervalRef.current = setInterval(() =&gt; {

timeRef.current += 1;

}, 1000);

};

const handleStop = () =&gt; {

isRunningRef.current = false;

clearInterval(intervalRef.current);

setDisplayTime(timeRef.current);

};

const handleReset = () =&gt; {

isRunningRef.current = false;

clearInterval(intervalRef.current);

timeRef.current = 0;

setDisplayTime(0);

};

return (

&lt;div&gt;

&lt;p&gt;Tempo: {displayTime}s&lt;/p&gt;

&lt;button onClick={handleStart}&gt;Iniciar&lt;/button&gt;

&lt;button onClick={handleStop}&gt;Parar e Exibir&lt;/button&gt;

&lt;button onClick={handleReset}&gt;Resetar&lt;/button&gt;

&lt;/div&gt;

);

}

export default StopWatch;</code></pre>

<p>Note que <code>timeRef.current</code> é incrementado 1000 vezes por segundo internamente, mas o componente não re-renderiza. Apenas quando você clica em &quot;Parar e Exibir&quot; é que <code>setDisplayTime</code> é chamado, disparando um render. Isso é extremamente eficiente: você evita 1000 re-renders desnecessários.</p>

<h3>Tracking de valores anteriores</h3>

<p>Um padrão avançado é usar <code>useRef</code> para rastrear o valor anterior de um estado. Isso é útil quando você precisa saber se algo mudou ou comparar valores entre renders.</p>

<pre><code class="language-javascript">import { useRef, useEffect, useState } from &#039;react&#039;;

function ComponenteComRastreamento() {

const [count, setCount] = useState(0);

const prevCountRef = useRef();

useEffect(() =&gt; {

prevCountRef.current = count;

}, [count]);

return (

&lt;div&gt;

&lt;p&gt;Agora: {count}&lt;/p&gt;

&lt;p&gt;Antes: {prevCountRef.current}&lt;/p&gt;

&lt;p&gt;Mudou: {count !== prevCountRef.current ? &#039;Sim&#039; : &#039;Não&#039;}&lt;/p&gt;

&lt;button onClick={() =&gt; setCount(count + 1)}&gt;Incrementar&lt;/button&gt;

&lt;/div&gt;

);

}

export default ComponenteComRastreamento;</code></pre>

<p>O <code>useEffect</code> executa <em>após</em> o render, então captura o valor antigo de <code>count</code> em <code>prevCountRef.current</code>. No próximo render, você consegue comparar o novo valor com o anterior. Esse padrão é base para implementar lógicas de &quot;se mudou, faça algo&quot;.</p>

<h2>Comunicação Entre Renders e Fluxos de Dados Complexos</h2>

<h3>Sincronizando múltiplos refs e estados</h3>

<p>Em aplicações mais complexas, você pode precisar sincronizar vários refs e estados para manter a lógica coerente. Um exemplo real é um formulário com validação que precisa rastrear valores, erros e estados de envio sem re-renderizar desnecessariamente.</p>

<pre><code class="language-javascript">import { useRef, useState } from &#039;react&#039;;

function AdvancedForm() {

const [submitting, setSubmitting] = useState(false);

const [submitted, setSubmitted] = useState(false);

const formRef = useRef(null);

const validationStateRef = useRef({

name: true,

email: true,

isValid: false

});

const fieldValuesRef = useRef({

name: &#039;&#039;,

email: &#039;&#039;

});

const validateField = (name, value) =&gt; {

let isValid = true;

if (name === &#039;name&#039;) {

isValid = value.trim().length &gt;= 3;

} else if (name === &#039;email&#039;) {

isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);

}

validationStateRef.current[name] = isValid;

fieldValuesRef.current[name] = value;

// Atualizar validação geral

validationStateRef.current.isValid =

validationStateRef.current.name &amp;&amp;

validationStateRef.current.email;

};

const handleChange = (e) =&gt; {

const { name, value } = e.target;

validateField(name, value);

};

const handleSubmit = async (e) =&gt; {

e.preventDefault();

if (!validationStateRef.current.isValid) {

alert(&#039;Formulário inválido&#039;);

return;

}

setSubmitting(true);

try {

// Simular chamada à API

await new Promise(resolve =&gt; setTimeout(resolve, 1000));

console.log(&#039;Dados enviados:&#039;, fieldValuesRef.current);

setSubmitted(true);

// Limpar após sucesso

setTimeout(() =&gt; {

setSubmitted(false);

formRef.current?.reset();

fieldValuesRef.current = { name: &#039;&#039;, email: &#039;&#039; };

validationStateRef.current = {

name: true,

email: true,

isValid: false

};

}, 2000);

} finally {

setSubmitting(false);

}

};

return (

&lt;form ref={formRef} onSubmit={handleSubmit}&gt;

&lt;div&gt;

&lt;input

type=&quot;text&quot;

name=&quot;name&quot;

placeholder=&quot;Nome&quot;

onChange={handleChange}

/&gt;

{!validationStateRef.current.name &amp;&amp; &lt;p style={{ color: &#039;red&#039; }}&gt;Nome deve ter 3+ caracteres&lt;/p&gt;}

&lt;/div&gt;

&lt;div&gt;

&lt;input

type=&quot;email&quot;

name=&quot;email&quot;

placeholder=&quot;Email&quot;

onChange={handleChange}

/&gt;

{!validationStateRef.current.email &amp;&amp; &lt;p style={{ color: &#039;red&#039; }}&gt;Email inválido&lt;/p&gt;}

&lt;/div&gt;

&lt;button

type=&quot;submit&quot;

disabled={submitting || !validationStateRef.current.isValid}

&gt;

{submitting ? &#039;Enviando...&#039; : &#039;Enviar&#039;}

&lt;/button&gt;

{submitted &amp;&amp; &lt;p style={{ color: &#039;green&#039; }}&gt;Formulário enviado com sucesso!&lt;/p&gt;}

&lt;/form&gt;

);

}

export default AdvancedForm;</code></pre>

<p>Aqui você vê múltiplos refs trabalhando juntos: <code>validationStateRef</code> rastreia a validade, <code>fieldValuesRef</code> armazena os valores do formulário, e <code>formRef</code> permite resetar o formulário. Nada disso dispara re-renders até que seja necessário (quando <code>setSubmitting</code> ou <code>setSubmitted</code> são chamados). Isso mantém o formulário responsivo mesmo com lógica complexa.</p>

<h3>Coordenando fluxos assíncronos</h3>

<p>Um cenário avançado é usar refs para coordenar múltiplas operações assíncronas sem que uma interfira na outra.</p>

<pre><code class="language-javascript">import { useRef, useState, useCallback } from &#039;react&#039;;

function DataFetcher() {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(false);

const requestIdRef = useRef(null);

const activeRequestsRef = useRef(new Set());

const fetchData = useCallback(async (url) =&gt; {

const currentRequestId = Symbol(&#039;request&#039;);

requestIdRef.current = currentRequestId;

activeRequestsRef.current.add(currentRequestId);

setLoading(true);

try {

const response = await fetch(url);

const result = await response.json();

// Só atualiza o estado se este request ainda é o mais recente

if (requestIdRef.current === currentRequestId) {

setData(result);

}

} catch (error) {

if (requestIdRef.current === currentRequestId) {

setData({ error: error.message });

}

} finally {

activeRequestsRef.current.delete(currentRequestId);

if (activeRequestsRef.current.size === 0) {

setLoading(false);

}

}

}, []);

return (

&lt;div&gt;

&lt;button onClick={() =&gt; fetchData(&#039;https://jsonplaceholder.typicode.com/posts/1&#039;)}&gt;

Fetch Post 1

&lt;/button&gt;

&lt;button onClick={() =&gt; fetchData(&#039;https://jsonplaceholder.typicode.com/posts/2&#039;)}&gt;

Fetch Post 2

&lt;/button&gt;

{loading &amp;&amp; &lt;p&gt;Carregando...&lt;/p&gt;}

{data &amp;&amp; (

&lt;div&gt;

&lt;h3&gt;{data.title | | &#039;Erro ao carregar&#039;}&lt;/h3&gt; &lt;p&gt;{data.body || data.error}&lt;/p&gt;

&lt;/div&gt;

)}

&lt;/div&gt;

);

}

export default DataFetcher;</code></pre>

<p>Este exemplo mostra race conditions resolvidas com refs. Quando você faz dois requests rapidamente, apenas o mais recente atualiza o estado. O ref <code>requestIdRef</code> rastreia qual é o request &quot;vencedor&quot; e <code>activeRequestsRef</code> monitora todas as requisições ativas. Sem isso, você enfrentaria bugs onde requests antigos sobrescrevem dados novos.</p>

<h2>Conclusão</h2>

<p>Aprendemos que <code>useRef</code> vai muito além de simplesmente acessar o DOM. Primeiro, é uma ferramenta para armazenar valores mutáveis que <strong>não disparam re-renders</strong>, permitindo otimizações drasticamente e facilitando integrações com APIs imperativos. Segundo, refs são excelentes para rastrear estado anterior, sincronizar múltiplos valores e resolver race conditions em operações assíncronas — tudo sem o overhead de renderização. Terceiro, o domínio de <code>useRef</code> avançado o coloca em um nível onde consegue escrever código React altamente eficiente e robusto, sabendo exatamente quando e por que usar refs em vez de estado.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react/useRef" target="_blank" rel="noopener noreferrer">React Official Documentation - useRef</a></li>

<li><a href="https://react.dev/reference/react/useRef" target="_blank" rel="noopener noreferrer">React Hooks API Reference - useRef</a></li>

<li><a href="https://dmitripavlutin.com/react-useref/" target="_blank" rel="noopener noreferrer">Dmitri Pavlutin - React useRef: The Complete Guide</a></li>

<li><a href="https://kentcdodds.com/blog/usehref-vs-usestate" target="_blank" rel="noopener noreferrer">Kent C. Dodds - When to useRef</a></li>

<li><a href="https://web.dev/dom/" target="_blank" rel="noopener noreferrer">Web.dev - Working with the DOM</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção
Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção

Entendendo Redux Toolkit: Fundamentos e Filosofia Redux é uma biblioteca de g...

Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State
Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State

Introdução: Os Quatro Pilares do Gerenciamento de Estado O gerenciamento de e...

Guia Completo de Animações em React: Framer Motion, React Spring e CSS Transitions
Guia Completo de Animações em React: Framer Motion, React Spring e CSS Transitions

Fundamentos de Animações em React Animações são um aspecto crítico da experiê...