Pular para o conteúdo
TypeScript

Tipar useRef no React com TypeScript 2026

useRef tem dois usos no React: referenciar elementos do DOM e guardar valores mutáveis entre renders. Cada um exige tipagem diferente. Aprenda os dois padrões, quando usar null

Por que isso é importante

Tipar useRef no React com TypeScript 2026. useRef tem dois usos no React: referenciar elementos do DOM e guardar valores mutáveis entre renders. Cada um exige tipagem diferente. Aprenda os dois padrões, quando usar null no valor inicial e como o TypeScript trata o .current em cada caso.

Os dois padrões de useRef

O useRef serve pra duas coisas completamente diferentes, e o TypeScript trata cada uma de forma distinta. O primeiro uso é referenciar elementos do DOM -- o famoso ref={meuRef} no JSX. O segundo é guardar valores mutáveis que persistem entre renders sem causar re-render (tipo um timer ID ou valor anterior).

A diferença na tipagem é sutil mas gera muita confusão. Pra DOM refs, você passa null como valor inicial e o TypeScript retorna RefObject com .current readonly. Pra refs mutáveis, você passa o valor inicial sem null (ou inclui null no generic) e recebe MutableRefObject com .current que aceita escrita.

Dá pra lembrar assim: se o React vai atribuir o valor (DOM ref), é readonly. Se você vai atribuir o valor (ref mutável), é mutável. O TypeScript decide isso baseado no tipo do generic vs o tipo do valor inicial.

// Padrão 1: DOM ref (readonly .current)
// null no valor inicial + HTMLElement no generic
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current é HTMLInputElement | null (readonly)

// Padrão 2: Ref mutável (writable .current)
// Valor inicial do mesmo tipo que o generic
const timerRef = useRef<number | null>(null);
// timerRef.current é number | null (mutável)

const countRef = useRef<number>(0);
// countRef.current é number (mutável)

Passo a passo: tipando useRef corretamente

  1. DOM ref: use o tipo HTML específico — Cada elemento tem seu tipo: HTMLInputElement, HTMLDivElement, HTMLButtonElement, etc. Declare useRef<HTMLInputElement>(null) e passe como ref={inputRef}.
  2. Cheque null antes de acessar .current — DOM refs começam null porque o elemento ainda não montou. Use if (ref.current) ou optional chaining ref.current?.focus() antes de chamar métodos.
  3. Ref mutável: inclua o tipo no generic — Para guardar timer IDs, valores anteriores ou flags: useRef<number>(0). O .current aceita atribuição direta.
  4. Use null no generic para ref mutável nullable — useRef<NodeJS.Timeout | null>(null). Inclua null no union do generic, não só no valor inicial.
  5. Passe a ref via forwardRef quando necessário — Componentes filhos recebem refs com React.forwardRef. Tipo o ref interno com o elemento correto.

Exemplos práticos: DOM refs e refs mutáveis

Focus automático com DOM ref

O caso de uso clássico: focar um input quando o componente monta. O tipo preciso do elemento garante que .focus(), .value e outras propriedades apareçam no autocomplete.

import { useRef, useEffect } from "react";

function CampoBusca() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Checa null porque o ref pode não estar atribuído ainda
    inputRef.current?.focus();
  }, []);

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="Buscar..." => console.log(e.target.value)}
    />
  );
}

Timer ref com cleanup

Galera, guardar IDs de setTimeout/setInterval no useRef é o padrão certo. Se guardar em variável local, perde a referência entre renders. Se guardar em state, causa re-render desnecessário.

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

function Debounce() {
  const [busca, setBusca] = useState("");
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    // Limpa timer anterior
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }

    // Cria novo timer
    timerRef.current = setTimeout(() => {
      console.log("Buscando:", busca);
    }, 500);

    // Cleanup no unmount
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    };
  }, [busca]);

  return (
    <input
      value={busca} => setBusca(e.target.value)}
      placeholder="Digite pra buscar..."
    />
  );
}

Ref de valor anterior

Outro padrão comum: guardar o valor anterior de um state ou prop. O ref atualiza depois do render, então sempre tem o valor da renderização anterior.

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

function usePrevious<T>(valor: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);

  useEffect(() => {
    ref.current = valor;
  });

  return ref.current;
}

// Uso
function Contador() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Atual: {count}</p>
      <p>Anterior: {prevCount ?? "nenhum"}</p>
      <button => setCount(c => c + 1)}>
        Incrementar
      </button>
    </div>
  );
}

forwardRef com TypeScript

Quando você cria um componente que precisa expor seu ref pro componente pai, usa forwardRef. O TypeScript exige dois generics: o tipo do ref e o tipo das props.

import { forwardRef } from "react";

interface InputCustomProps {
  label: string;
  erro?: string;
}

const InputCustom = forwardRef<HTMLInputElement, InputCustomProps>(
  ({ label, erro }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} className={erro ? "input-erro" : ""} />
        {erro && <span>{erro}</span>}
      </div>
    );
  }
);

// Uso no componente pai
function Formulario() {
  const nomeRef = useRef<HTMLInputElement>(null);

  return <InputCustom ref={nomeRef} label="Nome" />;
}

Erros comuns com useRef tipado

Evite essas ciladas

Esquecer null no valor inicial do DOM ref: useRef() sem null gera tipo errado. Sempre passe null: useRef(null).

Tentar atribuir .current em DOM ref: DOM refs são readonly. Se precisa de um ref mutável com elemento DOM, inclua null no generic: useRef(null).

Não checar null antes de acessar .current: DOM refs são null até o componente montar. Acesso direto sem check quebra em runtime.

Usar useRef pra state que precisa de re-render: useRef não causa re-render quando muda. Se a UI depende do valor, use useState.

Tipo HTML genérico demais: useRef funciona, mas perde métodos específicos. useRef dá acesso a .value, .focus(), .select(), etc.

Checklist: useRef tipado corretamente

Checklist de useRef + TypeScript

  • DOM ref usa o tipo HTML específico do elemento (HTMLInputElement, HTMLDivElement, etc.)
  • DOM ref passa null como valor inicial
  • Acesso a .current checa null antes (if ou optional chaining)
  • Ref mutável declara o tipo correto no generic
  • Ref mutável nullable inclui null no generic, não só no valor inicial
  • Timer refs usam ReturnType para compatibilidade
  • forwardRef declara os dois generics: tipo do ref e tipo das props
  • useRef não é usado para valores que deveriam ser state (que afetam renderização)

Refs sem mistério

useRef tipado corretamente destranca cenários avançados: focus management, animações, integrações com libs externas e custom hooks poderosos. No CrazyStack, você pratica tudo isso em projetos reais -- do formulário com validação ao player de vídeo customizado. Domine hooks com tipagem profissional.