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 writeref.currentwhenever you want. - Changing
ref.currentdoesn'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
refattribute of a JSX element, React places the real node incurrentafter mounting. That lets you focus, measure sizes, or scroll. - Typing: for the DOM you use
useRef<HTMLInputElement>(null). It starts asnullbecause 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
currentproperty 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 asuseRef<HTMLInputElement>(null), pass it to the element'srefattribute, 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.
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>
);
}