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:
refis escape hatch. Using it sparingly.
Ref vs State
-
stateandrefcould point to anything: a string, an object, or even a function. -
stateandrefsboth are live outside of your component. -
stateandrefsboth are retained by React between re-renders. - Mutating
statecause re-render. Mutatingrefwon’t. -
stateworks as snapshot for each render, You can’t get latest state from an asynchronous operation; Butrefwon’t be affected by render, you can read the latest ref anytime. -
stateis ”Immutable” — you must use the state setting function to modify state variables to queue a re-render;refis mutable, it is a plain JavaScript object that you can read and modify. - You shouldn’t read (or write) the
ref.currentvalue during rendering. But You can readstateany 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.currentduring rendering. If some information is needed during rendering, use state instead. Since React doesn’t know whenref.currentchanges, 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
refas therefattribute 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:
refdoesn’t hold a single DOM node. Instead, it holds aMapfrom item ID to a DOM node. Therefcallback 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
refcallback 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 secondrefargument which is declared after props. - MyInput itself passes the
refit 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.