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 and ref could point to anything: a string, an object, or even a function.
  • state and refs both are live outside of your component.
  • state and refs both are retained by React between re-renders.
  • Mutating state cause re-render. Mutating ref won’t.
  • state works as snapshot for each render, You can’t get latest state from an asynchronous operation; But ref 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 read state 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 when ref.current changes, even reading it while rendering makes your component’s behavior difficult to predict. (The only exception to this is code like if (!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 the ref 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 a Map from item ID to a DOM node. The ref 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 the ref.

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 second ref 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 the ref

When React attaches the refs

In React, every update is split in two phases:

  1. During render, React calls your components to figure out what should be on the screen.
  2. 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.

Reference

React official doc