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

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

O que é useRef e por que vai além de useState

O useRef é um hook do React que retorna um objeto mutável cuja propriedade .current persiste entre renders. Diferente de useState, 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 useRef é tão poderoso é que ele quebra o ciclo de dados unidirecional do React. Enquanto useState força a filosofia "estado muda → componente re-renderiza", useRef 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 useRef. Você verá que, apesar de simples em conceito, a aplicação correta do hook resolve problemas que seriam complicados com useState.

Acessando e Manipulando o DOM Diretamente

Referências a elementos DOM

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

import { useRef } from 'react';

function TextInputWithFocus() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    if (inputRef.current) {
      inputRef.current.focus();
      inputRef.current.style.borderColor = 'blue';
    }
  };

  return (
    <>
      <input ref={inputRef} type="text" placeholder="Digite algo..." />
      <button onClick={handleFocus}>Focar no input</button>
    </>
  );
}

export default TextInputWithFocus;

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

Integrando com bibliotecas externas

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. useRef é a ponte perfeita entre o mundo declarativo do React e essas bibliotecas.

import { useRef, useEffect } from 'react';
import Chart from 'chart.js/auto';

function BarChart() {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);

  useEffect(() => {
    if (canvasRef.current && !chartRef.current) {
      chartRef.current = new Chart(canvasRef.current, {
        type: 'bar',
        data: {
          labels: ['Jan', 'Fev', 'Mar', 'Abr'],
          datasets: [{
            label: 'Vendas',
            data: [12, 19, 3, 5],
            backgroundColor: 'rgba(75, 192, 192, 0.2)',
            borderColor: 'rgba(75, 192, 192, 1)',
            borderWidth: 1
          }]
        }
      });
    }

    return () => {
      if (chartRef.current) {
        chartRef.current.destroy();
      }
    };
  }, []);

  return <canvas ref={canvasRef} width="400" height="200"></canvas>;
}

export default BarChart;

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

Valores Mutáveis e Persistência Entre Renders

Armazenando valores sem disparo de re-renders

Um uso avançado de useRef é 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. useRef permite isso sem criar renders desnecessários.

import { useRef, useState } from 'react';

function StopWatch() {
  const [displayTime, setDisplayTime] = useState(0);
  const timeRef = useRef(0);
  const intervalRef = useRef(null);
  const isRunningRef = useRef(false);

  const handleStart = () => {
    if (isRunningRef.current) return;

    isRunningRef.current = true;
    intervalRef.current = setInterval(() => {
      timeRef.current += 1;
    }, 1000);
  };

  const handleStop = () => {
    isRunningRef.current = false;
    clearInterval(intervalRef.current);
    setDisplayTime(timeRef.current);
  };

  const handleReset = () => {
    isRunningRef.current = false;
    clearInterval(intervalRef.current);
    timeRef.current = 0;
    setDisplayTime(0);
  };

  return (
    <div>
      <p>Tempo: {displayTime}s</p>
      <button onClick={handleStart}>Iniciar</button>
      <button onClick={handleStop}>Parar e Exibir</button>
      <button onClick={handleReset}>Resetar</button>
    </div>
  );
}

export default StopWatch;

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

Tracking de valores anteriores

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

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

function ComponenteComRastreamento() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>Agora: {count}</p>
      <p>Antes: {prevCountRef.current}</p>
      <p>Mudou: {count !== prevCountRef.current ? 'Sim' : 'Não'}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  );
}

export default ComponenteComRastreamento;

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

Comunicação Entre Renders e Fluxos de Dados Complexos

Sincronizando múltiplos refs e estados

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.

import { useRef, useState } from 'react';

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: '',
    email: ''
  });

  const validateField = (name, value) => {
    let isValid = true;

    if (name === 'name') {
      isValid = value.trim().length >= 3;
    } else if (name === 'email') {
      isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    }

    validationStateRef.current[name] = isValid;
    fieldValuesRef.current[name] = value;

    // Atualizar validação geral
    validationStateRef.current.isValid = 
      validationStateRef.current.name && 
      validationStateRef.current.email;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    validateField(name, value);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!validationStateRef.current.isValid) {
      alert('Formulário inválido');
      return;
    }

    setSubmitting(true);

    try {
      // Simular chamada à API
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('Dados enviados:', fieldValuesRef.current);
      setSubmitted(true);

      // Limpar após sucesso
      setTimeout(() => {
        setSubmitted(false);
        formRef.current?.reset();
        fieldValuesRef.current = { name: '', email: '' };
        validationStateRef.current = {
          name: true,
          email: true,
          isValid: false
        };
      }, 2000);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          name="name"
          placeholder="Nome"
          onChange={handleChange}
        />
        {!validationStateRef.current.name && <p style={{ color: 'red' }}>Nome deve ter 3+ caracteres</p>}
      </div>

      <div>
        <input
          type="email"
          name="email"
          placeholder="Email"
          onChange={handleChange}
        />
        {!validationStateRef.current.email && <p style={{ color: 'red' }}>Email inválido</p>}
      </div>

      <button 
        type="submit" 
        disabled={submitting || !validationStateRef.current.isValid}
      >
        {submitting ? 'Enviando...' : 'Enviar'}
      </button>

      {submitted && <p style={{ color: 'green' }}>Formulário enviado com sucesso!</p>}
    </form>
  );
}

export default AdvancedForm;

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

Coordenando fluxos assíncronos

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

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

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) => {
    const currentRequestId = Symbol('request');
    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 (
    <div>
      <button onClick={() => fetchData('https://jsonplaceholder.typicode.com/posts/1')}>
        Fetch Post 1
      </button>
      <button onClick={() => fetchData('https://jsonplaceholder.typicode.com/posts/2')}>
        Fetch Post 2
      </button>

      {loading && <p>Carregando...</p>}
      {data && (
        <div>
          <h3>{data.title || 'Erro ao carregar'}</h3>
          <p>{data.body || data.error}</p>
        </div>
      )}
    </div>
  );
}

export default DataFetcher;

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

Conclusão

Aprendemos que useRef vai muito além de simplesmente acessar o DOM. Primeiro, é uma ferramenta para armazenar valores mutáveis que não disparam re-renders, 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 useRef 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.

Referências


Artigos relacionados