author-avatar-imageGorkem Arpaci
20 Mar, 202410m read time

Refs: Take DOM Control from React

author-avatar-image

React handles DOM updates automatically to reflect the render output of your components, reducing the need for direct manipulation. However, in certain cases, you may require access to the DOM elements managed by React, such as focusing on a node, scrolling to it, or determining its size and position. Since React doesn't provide built-in methods for these tasks, you'll need to use a ref to access the DOM node directly.

Accessing a node with ref

To access a DOM node managed by React, you need to first create a ref instance with the help of useRef hook, then pass it to desired element.

Component.jsx

import { useRef } from 'react';
// ...

const Component = () => {
  const myRef = useRef(null);
  //...

  return (
    //...
    <div ref={myRef}>
  )
}

The useRef Hook creates an object with a single property which is current. myRef.current will be null initially. When React creates a DOM node for this <div>, React will pass a reference to this node into myRef.current. After that you can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.

For example, let's lets consider scrollIntoView API:

myRef.current.scrollIntoView();

Text input focus

In the below illustration, clicking the button will focus the input:

Form.jsx

import { useRef } from 'react';

const Form = () => {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <main>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </main>
  );
}

export default Form;

Let's clarify the steps:

  1. First we declared a variable called inputRef with the useRef hook.
  2. After we passed it as <input ref={inputRef}>. This makes React to put this <input>’s DOM node reference into inputRef.current.
  3. Listen the onClick event by passing the handleClick event handler to onClick prop of <button>.
  4. Get the input DOM node from inputRef.current in the handleClick function and run inputRef.current.focus().

Scrolling to an element

Let's move on with a case where multiple refs exist. In the below illustration, there are three images and three buttons. Each button centers corresponding image by using the browser scrollIntoView() method on the corresponding DOM node:

Carousel.jsx

import { useRef } from 'react';

const Carousel = () => {
  const firstImgRef = useRef(null);
  const secondImgRef = useRef(null);
  const thirdImgRef = useRef(null);

  const scrollBehavior = {
    behavior: 'smooth',
    block: 'nearest',
    inline: 'center'
  }

  const handleScrollToFirstImg = () => {
    firstImgRef.current.scrollIntoView(scrollBehavior);
  }

  const handleScrollToSecondImg = () => {
    secondImgRef.current.scrollIntoView(scrollBehavior);
  }

  const handleScrollToThirdImg = () => {
    thirdImgRef.current.scrollIntoView(scrollBehavior);
  }

  return (
    <section>
      <nav>
        <button onClick={handleScrollToFirstImg}>
          Tom
        </button>
        <button onClick={handleScrollToSecondImg}>
          Maru
        </button>
        <button onClick={handleScrollToThirdImg}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="first-img-url"
              alt="first-img"
              ref={firstImgRef}
            />
          </li>
          <li>
            <img
              src="second-img-url"
              alt="second-img"
              ref={secondImgRef}
            />
          </li>
          <li>
            <img
              src="third-img-url"
              alt="third-img"
              ref={thirdImgRef}
            />
          </li>
        </ul>
      </div>
    </section>
  );
}

Passing refs to other components as prop

Ok, so far we only covered using refs inside of a component and passing them to built-in HTML elements like <div />, <input />, etc.

What about passing ref to some custom component, like <InputComponent />. If you try to pass a ref on your own custom component, by default you will get null. Below is an illustration of it:

InputComponent.jsx

const InputComponent = (props) => {
  return <input {...props} />;
}

Form.jsx

import {useRef} from 'react';
import InputComponent from "./InputComponent.jsx"

const Form = () => {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  }

  return (
    <div>
      <InputComponent ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </div>
  );
}

If we run this code and click the button, you will notice input does not get focus state when button is clicked.
React also prints an error to the console to help you notice the issue:

author-avatar-image


This occurs because React inherently restricts a component from directly accessing the DOM nodes of other components, including its own children. This restriction is intentional. Refs serve as an escape mechanism but should be employed carefully. Engaging in manual manipulation of another component's DOM nodes increases the fragility of your code.

Instead, components that want to expose their DOM nodes have to adapt to that behavior. By using forwardRef, component can indicate that it forwards its ref to one of its children.

Below is an illustration of how InputComponent is adjusted to this situation with using forwardRef API:

InputComponent.jsx

const InputComponent = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

Let's clarify this process:

  1. <InputComponent ref={inputRef} /> tells React to pass the corresponding DOM node into inputRef.current. However, it's the responsibility of the InputComponent to opt into that behavior; by default, it doesn't.
  2. The InputComponent is constructed with forwardRef. This makes available it into receiving the inputRef from above as the second ref argument which is declared after props.
  3. InputComponent itself passes the ref it received to the <input> inside of it.

Now, clicking the button to focus the input operates as expected.

When to access refs during component's lifecycle

In React, each update cycle comprises two phases:

  1. Render Phase: React invokes your components to determine the content of screen.
  2. Commit Phase: React applies changes to the DOM based on the render results.

It's generally not recommended to access refs during rendering, including those containing DOM nodes. During the initial render, DOM nodes haven't been created yet, so ref.current will be null. Similarly, during subsequent updates, the DOM nodes haven't been updated, making it stale to read them.

React assigns values to ref.current during the commit phase. Before updating the DOM, React resets the affected ref.current values to null. After updating the DOM, React immediately assigns them the corresponding DOM nodes.

Usually, refs are accessed from event handlers as a best practice.

Recap

  • Although refs are common concept, generally you’ll use them to manipulate DOM elements.
  • The way to make React to put a DOM node into myRef.current is by passing <div ref={myRef}>.
  • Most of the time, refs are used for non-destructive actions like focusing, scrolling, or measuring DOM elements provided by browser APIs.
  • By default, components doesn’t expose its DOM nodes. They need to be adapted to that expose process by using forwardRef and passing the second ref argument down to a related node.
  • First rule of RefClub: Avoid changing DOM nodes controlled by React.
  • If you do modify DOM nodes controlled by React, only update parts that React has no reason to update.

Sources:

React.dev

MDN

Thank you for reading this far. See you in the next chapter :)

Comments
Be the first to commentNobody's responded to this post yet.Add your thoughts and get the conversation going.