We’ve said that in React, the data flow is declarative. You change the state and the interface updates automatically.
However, there are still situations where we need to step outside this flow, to interact imperatively with the browser or to maintain persistence without triggering renders.
This is where we use useRef.
Basically, it’s the “escape hatch” tool that React gives us to manage what we would do with variables without React, without affecting the usual render cycle.
What is useRef?
Technically, a reference (ref) is a mutable container whose value persists throughout the entire lifecycle of the component. The basic syntax is:
const ref = useRef(initialValue);
- initialValue: Initial value of the reference (can be
null, a number, an object, etc.). - ref.current: Property that stores the current value and can be modified directly.
The key is that this object { current: ... } is the same memory instance throughout the component’s life. React guarantees it will always return the same object, no matter what.
When to use useRef
This Hook has two fundamental purposes that cover the limitations of useState:
- Accessing the DOM: Allows getting a direct reference to an HTML node to execute native methods.
- Persistence without re-render: Allows storing values that survive re-renders but, when changed, do not cause a new visual update.
Let’s look at each one 👇.
Use 1: Accessing the Real DOM
React is declarative. When we want an <input>, we generally don’t want to touch it. We just link its value to a state. But sometimes we need to touch the DOM element.
The most typical case is managing focus. Imagine you want the cursor to automatically be placed inside an input when the page loads or a button is pressed.
There is no focus={true} prop in HTML that we can easily turn on/off. So we need to call the native .focus() method of the DOM element.
This is where we can use useRef.
Create the reference with an initial value of null.
Connect it to the JSX using the special ref attribute.
Access it using .current when we need it.
import { useRef } from 'react';
export default function FormularioFoco() {
// 1. Create the ref
const inputRef = useRef(null);
const handleClick = () => {
// 3. Access the real DOM element
// inputRef.current is the browser's <input> element
inputRef.current.focus();
// We can do anything native:
// inputRef.current.scrollIntoView();
// inputRef.current.style.backgroundColor = 'red'; (Although we shouldn't abuse this)
};
return (
<div>
{/* 2. Connect the ref to the element */}
<input ref={inputRef} type="text" />
<button onClick={handleClick}>
Focus
</button>
</div>
);
}
Do not manipulate the DOM with refs for things you could do with props or state (like changing text or CSS classes).
Use refs only for the “weird stuff” that requires us to use imperative methods (focus, media playback, measuring positions).
Use 2: Mutable Variables (Persistence without render)
Sometimes we need to save a value, but we don’t want changing it to render the component. Let’s see what options we have.
let: Resets on every render. Has no memory.useState: Has memory, but triggers a re-render when changed.useRef: Has memory and is silent (does not trigger re-render).
That is, put in table form:
| Characteristic | Variable let | useState | useRef |
|---|---|---|---|
| Persists between renders? | ❌ No | ✅ Yes | ✅ Yes |
| Has memory? | ❌ No | ✅ Yes | ✅ Yes |
| Triggers re-render when changed? | ❌ No | ✅ Yes | ❌ No |
But let’s see it better with an example. The classic is a stopwatch. We need to save the ID of the setInterval to be able to stop it (clearInterval) later.
But the interval ID is an “internal number”. The user doesn’t care if the ID is 123 or 456. We don’t need to repaint the screen just because that ID changed.
import { useState, useRef } from 'react';
export default function Cronometro() {
const [segundos, setSegundos] = useState(0);
// We use a ref to store the interval ID
// If we used useState, we would cause unnecessary extra renders.
// If we used 'let intervalId', we would lose the value when re-rendering due to seconds.
const intervalRef = useRef(null);
const iniciar = () => {
// Avoid creating multiple intervals
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => {
setSegundos(s => s + 1);
}, 1000);
};
const detener = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
return (
<div>
<h1>Time: {segundos}s</h1>
<button onClick={iniciar}>Start</button>
<button onClick={detener}>Stop</button>
</div>
);
}
In this example, intervalRef.current acts like an instance variable of an old class. It persists, it’s there, we can read and write it, but React ignores it for drawing purposes.
When to use which?
To know if you need useState or useRef, ask yourself this question. “If I change this data, should what is seen on the screen change?”
- YES ➡ Use
useState. - NO ➡ Use
useRef.
Common mistakes
There is a very serious mistake you must avoid: Reading or writing refs during rendering.
React assumes that the body of your component (the function) must be pure and have no side effects. Modifying a ref is a side effect.
function Componente() {
const contador = useRef(0);
// ¡ERROR! Modifying ref during render
contador.current = contador.current + 1;
return <h1>{contador.current}</h1>; // ¡ERROR! Reading ref in JSX
}
Make changes inside Event Handlers or Effects.
function Componente() {
const contador = useRef(0);
useEffect(() => {
// Correct: Side effect after render
contador.current = contador.current + 1;
});
// ...
}
