What is Refs
The Definition
When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a
ref
.
You can access the current value of that ref through the ref.current
property. This value is intentionally mutable, meaning you can both read and write to it. It’s like a secret pocket of your component that React doesn’t track. (This is what makes it an “escape hatch” from React’s one-way data flow—more on that below!)
Refs are a generic concept, but most often you’ll use them to hold DOM elements.
But you should always keep this in mind:
ref
is escape hatch. Using it sparingly.
Ref vs State
-
state
andref
could point to anything: a string, an object, or even a function. -
state
andrefs
both are live outside of your component. -
state
andrefs
both are retained by React between re-renders. - Mutating
state
cause re-render. Mutatingref
won’t. -
state
works as snapshot for each render, You can’t get latest state from an asynchronous operation; Butref
won’t be affected by render, you can read the latest ref anytime. -
state
is ”Immutable” — you must use the state setting function to modify state variables to queue a re-render;ref
is mutable, it is a plain JavaScript object that you can read and modify. - You shouldn’t read (or write) the
ref.current
value during rendering. But You can readstate
any time.
Using ref
Example: alerting how many click have happen.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
DebouncedButton component
If you keep clicking the button, it will ignore the click event, untill you stop and wait a second. This is called Debounced. ref
is good way to implement this:
import {useRef} from 'react';
function DebouncedButton({ onClick, children }) {
const timeoutID = useRef(null);
return (
<button onClick={() => {
clearTimeout(timeoutID.current);
timeoutID.current = setTimeout(() => {
onClick();
}, 1000);
}}>
{children}
</button>
);
}
When to use refs
Typically, you will use a ref when your component needs to “step outside” React and communicate with external APIs—often a browser API that won’t impact the appearance of the component. Here are a few of these rare situations:
- Storing and manipulating DOM elements (the most common use case).
- Storing timeout IDs
- Storing other objects that aren’t necessary to calculate the JSX.
If your component needs to store some value, but it doesn’t impact the rendering logic, choose refs.
Best practices for refs
Following these principles will make your components more predictable:
- Treat refs as an escape hatch. Refs are useful when you work with external systems or browser APIs. If much of your application logic and data flow relies on refs, you might want to rethink your approach.
- Don’t read or write
ref.current
during rendering. If some information is needed during rendering, use state instead. Since React doesn’t know whenref.current
changes, even reading it while rendering makes your component’s behavior difficult to predict. (The only exception to this is code likeif (!ref.current) ref.current = new Thing()
which only sets the ref once during the first render.)
Manipulating the DOM with Refs
React automatically updates the DOM to match your render output, so your components won’t often need to manipulate it. However, sometimes you might need access to the DOM elements managed by React, for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref
to the DOM node.
You can instruct React to put a DOM node into myRef.current
by passing <div ref={myRef}>
. Once the element is removed from the DOM, React will update myRef.current
to be null
.
Getting a ref
to the node
To access a DOM node managed by React:
- first, import the useRef Hook:
import { useRef } from 'react';
- Then, use it to declare a ref inside your component:
const myRef = useRef(null);
- Finally, pass your
ref
as theref
attribute to the JSX tag for which you want to get the DOM node:<div ref={myRef}>
The useRef
Hook returns an object with a single property called current. Initially, myRef.current
will be null
.
When React creates a DOM node for this <div>
, React will put a reference to this node into myRef.current
. You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it. Then:
// You can use any browser APIs, for example:
myRef.current.scrollIntoView();
The ref
callback
Sometimes you might need a ref
to each item in the list, and you don’t know how many you will have.
ref
callback can let you manage a list of refs!
The Basic Idea is like this:
ref
doesn’t hold a single DOM node. Instead, it holds aMap
from item ID to a DOM node. Theref
callback on every list item takes care to update the Map.
You can pass a callback function to the ref
attribute. And then React will call your ref
callback with the DOM node when it’s time to set the ref
, and with null
when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref
by its index or some kind of ID.
The tricky thing is
React will call your
ref
callback with the DOM node when it’s time to set theref
.
It works liks this:
// 1. define ref
const itemsRef = useRef(null);
// 2. get ref.current reference.
function getMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map();
}
return itemsRef.current;
}
// 3. tell React how to arrange the ref to nodes list.
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
return (
<>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
map.set(cat.id, node);
} else {
map.delete(cat.id);
}
}}
>
<img
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
// 4. now you can access individual DOM nodes from the Map according to ID:.
function getNodeByID(itemId) {
const map = getMap();
const node = map.get(itemId);
return node;
}
Accessing component’s DOM nodes
When you put a ref
on a built-in component that outputs a browser element like <input />
, React will set that ref
’s current
property to the corresponding DOM node (such as the actual <input />
in the browser).
However, if you try to put a ref
on your own component, like <MyInput />
, by default you will get null
. This is intentional.
Accessing component’s DOM nodes, you should use React.forwardRef()
React.forwardRef()
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
This is how it works:
-
<MyInput ref={inputRef} />
tells React to put the corresponding DOM node into inputRef.current. However, it’s up to the MyInput component to opt into that—by default, it doesn’t. - The MyInput component is declared using
forwardRef
. This opts it into receiving the inputRef from above as the secondref
argument which is declared after props. - MyInput itself passes the
ref
it received to the<input>
inside of it.
useImperativeHandle()
This function can restrict the exposed functionality of theref
When React attaches the refs
In React, every update is split in two phases:
- During render, React calls your components to figure out what should be on the screen.
- During commit, React applies changes to the DOM.
React sets ref.current
during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.
In general, you don’t want to access refs
during rendering. That goes for refs
holding DOM nodes as well. During the first render, the DOM nodes have not yet been created, so ref.current
will be null
. And during the rendering of updates, the DOM nodes haven’t been updated yet. So it’s too early to read them.
But some rare cases, you may want React to update the DOM synchronously. For example, you add a ‘todo’ to ‘todoslist’, and after adding, show that ‘todo’:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
The code above won’t work. Because the new todo item haven’t added to the DOM node. One way to fix this issue can be:
import { flushSync } from 'react-dom';
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Flushing state updates synchronously with flushSync
Best practices for DOM manipulation with refs
-
Refs are an escape hatch. You should only use them when you have to “step outside React”. Common examples of this include managing focus, scroll position, or calling browser APIs that React does not expose.
-
If you stick to non-destructive actions like focusing and scrolling, you shouldn’t encounter any problems. However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making.