← React & TypeScript

Hooks

What is useRef and how is it different from useState?

Show answer — try answering out loud first

useRef gives you a mutable box ({ current: ... }) that survives across renders but does NOT trigger a re-render when it changes. useState also survives, but every update does re-render the component.

Think of useRef as an instance variable: a place to store something you need to remember across renders, but that doesn't affect what gets painted on screen.

The key points:

  • It always returns the same object { current: initialValue } on every render. You can read and write ref.current whenever you want.
  • Changing ref.current doesn't re-render. That's why it's useful for timer IDs, internal counters, flags like "has it mounted yet?", or the previous value of a prop.
  • DOM references: if you pass the ref to the ref attribute of a JSX element, React places the real node in current after mounting. That lets you focus, measure sizes, or scroll.
  • Typing: for the DOM you use useRef<HTMLInputElement>(null). It starts as null because the node doesn't exist until React mounts; that's why you check it with ?. or an if when using it.
  • Golden rule: if the value is shown in the UI, it's useState. If you only need to remember it across renders, it's useRef.
  • Don't read or write refs during render: do it in handlers or effects, so the render stays pure.
import { useRef, useState } from "react";

function BuscadorConFoco() {
  // DOM ref: starts as null until React mounts the input
  const inputRef = useRef<HTMLInputElement>(null);

  // Ref as a mutable value: counts clicks WITHOUT triggering renders
  const clicsSinRender = useRef(0);

  const [busqueda, setBusqueda] = useState("");

  function enfocarInput() {
    clicsSinRender.current += 1; // doesn't re-render
    console.log(`Clics acumulados: ${clicsSinRender.current}`);
    inputRef.current?.focus(); // direct access to the real node
  }

  return (
    <div>
      <input
        ref={inputRef}
        value={busqueda}
        onChange={e => setBusqueda(e.target.value)} // this DOES re-render
        placeholder="Escribe tu búsqueda"
      />
      <button onClick={enfocarInput}>Enfocar</button>
      {/* Expected behavior: on click, the input gets focus.
          The ref counter goes up in the console, but the UI is not repainted
          by that change: it only repaints when "busqueda" changes. */}
    </div>
  );
}

Using a ref for something that should be visible on screen:

// WRONG: the UI never updates because changing the ref doesn't re-render
const contador = useRef(0);
return (
  <button onClick={() => { contador.current += 1; }}>
    Clics: {contador.current} {/* stays frozen on screen */}
  </button>
);

// RIGHT: if it's shown in the UI, it's state
const [contador, setContador] = useState(0);
return (
  <button onClick={() => setContador(prev => prev + 1)}>
    Clics: {contador}
  </button>
);

The reverse mistake also exists: using useState for a setInterval ID generates useless renders every time you store it. Nobody sees that ID: it belongs in a ref.

useRef gives me a mutable object with a current property that persists across renders, but with a key difference from useState: modifying the ref doesn't trigger a re-render. That's why I use it for values I need to remember but that don't get painted, like timer IDs or the previous value of a prop. Its other main use is accessing DOM elements: I type it as useRef<HTMLInputElement>(null), pass it to the element's ref attribute, and after mounting I can focus it or measure it. My mental rule is: if the data appears in the UI, useState; if I just need to remember it across renders, useRef.

Quick challenge

Create a ReproductorSimple component with a <video> and two buttons: "Reproducir" and "Pausar", using a ref typed to the video element.

See solution
import { useRef } from "react";

function ReproductorSimple() {
  // HTMLVideoElement exposes play() and pause()
  const videoRef = useRef<HTMLVideoElement>(null);

  return (
    <div>
      <video ref={videoRef} src="/demo.mp4" width={320} />
      <button onClick={() => videoRef.current?.play()}>Reproducir</button>
      <button onClick={() => videoRef.current?.pause()}>Pausar</button>
      {/* Note: we use ?. because current is null until the video mounts */}
    </div>
  );
}