DEV Community

Cover image for Things you need to know about React ref
Romain Trotard
Romain Trotard

Posted on • Edited on • Originally published at romaintrotard.com

Things you need to know about React ref

In my previous article, I talked about React state. Now it's time to discuss about React reference:

  • What is it?
  • How to use them?
  • When to use it?
  • How does it work under the hood?

Let's go.


What is it?

A React reference is simply an object that has it's reference which is fixed during component renders and that that a key current that is mutated.

Unlike React state, when we change a reference (mutates it) React WILL NOT trigger a re-render of the component.


How to use it?

Before version 16.8.6 of React it was possible only to use ref on class component.

Class component

To create a reference in a Class component you only have to call:

import React from 'react';

const ref = React.createRef();
Enter fullscreen mode Exit fullscreen mode

Call it in:

  • the constructor:
class MyClassComponent extends React.Component {
  constructor() {
    this.myRef = React.createRef();
  }

  render() {
    return <p>A simple class component with a ref</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • directly declaring the property name you want:
class MyClassComponent extends React.Component {
  myRef = React.createRef();

  render() {
    return <p>A simple class component with a state</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: You can use both as you wish. It's the same. But constructor will give you the possibility to use props to initialize the ref:

class MyClassComponent extends React.Component {
  constructor(props) {
    this.myRef = React.createRef();
    this.myRef.current = props.someValue;
  }

  render() {
    return <p>A simple class component with a ref</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Functional component

After 16.8.6, hooks have been introduced, especially useRef:

import { useRef } from 'react';

const ref = useRef(initValue);
Enter fullscreen mode Exit fullscreen mode

With a component you will have:

import { useRef } from "react";

function StateFunctionalComponent() {
  // myRef will have a fixed reference
  // The initial value is 0
  const myRef = useRef(0);

  return <p>Functional component with state</p>;
}
Enter fullscreen mode Exit fullscreen mode

Access and update

Then, once you have created the reference, you probably want to get the value and update it.
You will just work with the current property:

const myRef = useRef();

// Get the value
console.log('The value is:', myRef.current);

// Update the value
myRef.current = 'New value';
Enter fullscreen mode Exit fullscreen mode

Warning: Do not update the reference in the render directly. You will see in the next part where to do it and when it is useful.


What should not be done with ref?

I spoiled it a little at the end of the previous part, you should never update/read a reference inside the render directly, the only exception is for lazy initialization.

What is lazy initialization?

Lazy init is when you check if the ref has not value to set one. It's useful for example when you work with Portal to get the container:

function MyComponent() {
  const container = useRef();

  if (!container) {
    container.current =
      document.getElementById("myContainer");
  }

  return ReactDOM.createPortal(
    <p>Will be inside the element with id: myContainer</p>,
    container.current
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: If you do not know what a Portal is, here is the React definition:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

Why should you not update/read in render?

It's because of incoming concurrent rendering. With concurrent mode, the rendering process will not be synchronous anymore, so it will be possible that rendering of some component is "paused" to keep as most as possible 60 frames per second and a nice interactivity feeling.
So it would be possible to make inconcistency if a ref is used inside render for UI (because we mutate an object).
Whereas React will ensure there is no inconcistency with React states.

To help you to identify where are problems with ref, there will be some warning in the console about that. You can see this PR: useRef: Warn about reading or writing mutable values during render that introduce the warnings.


When to use it?

Okay now that we know what is it and that the component will not re-render after mutation of the reference, when is it useful?

There is multiple cases, let's see them.

Note: In all the example I will code with Functional component but you can do the same with Class component

Get reference to DOM element

The main role of reference is to have access to a DOM element and then be able to do some process on the element like: focus, get the value of an input, ...

In this case, you have to put the ref on the "React DOM element".

function MyComponent() {
  const inputRef = useRef();

  return <input type="text" ref={inputRef} />;
}
Enter fullscreen mode Exit fullscreen mode

Then you have access to the real DOM element through ref.current.

For example, with the input we can get the value filled by the user:

function MyComponent() {
  const inputRef = useRef();

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button
        type="button"
        onClick={() =>
          console.log(
            "The value is:",
            inputRef.current.value
          )
        }
      >
        Show the value
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: Get element with useRef if the element to select is here at the mount.

If the element is conditionally rendered, you would probably prefer to use a ref callback

function MyComponent() {
  const [show, setShow] = useState(false);

  const refCallback = useCallback((node) => {
    if (!node) {
      console.log("The node is unmounted");
    } else {
      console.log("The node is", node);
    }
  }, []);

  return (
    <div>
      <button
        type="button"
        onClick={() => setShow((prev) => !prev)}
      >
        Show / unshow
      </button>
      {show && (
        <div ref={refCallback}>
          Element with ref callback
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: You can see that I useCallback the ref callback otherwise the method will called at each render that will be bad for the behavior's consistency. I you want other cases where to use useCallback, you can see my article When to use useCallback.

Note: If you want to pass the ref to a component you will have to choice with FC: pass it with different name than ref or forwardRef on the component. In CC you will have to name it differently.

// Forward the ref
const FunctionalComponent = React.forwardRef(
  (props, ref) => {
    // Content of component
  }
);

// Different name
function FunctionalComponent({ customRef }) {
  // Content of component
}
Enter fullscreen mode Exit fullscreen mode

Store data not useful for UI (used in event listener for example)

One other case is to store value that does not need to trigger a re-render, for example when you use it only in event listener.

Let's take the example where you want to prevent clicking on a button (but not show a different style), in this case let's use a ref:

function MyComponent() {
  const preventClick = useRef(false);

  return (
    <div>
      <button
        type="button"
        onClick={() =>
          (preventClick.current = !preventClick.current)
        }
      >
        Enable / Disable click
      </button>
      <button
        type="button"
        onClick={() => {
          if (preventClick.current) {
            return;
          }

          console.log("You are able to click");
        }}
      >
        Will you be able to click?
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: In reality, we would probably show a different style which will make us use a state instead.


Get latest value of a value in useCallback

Sometimes I do not want to useCallback some function for example when doing memoization for performances.

For example:

const callback = useCallback(() => {
  console.log("I use the dep:", value);
}, [value]);
Enter fullscreen mode Exit fullscreen mode

This callback will be recreated, each time value is changing. But most of the time I do not want that. For example when the callback is used as an event handler.

So in this case, I will put the value in a ref that will ensure me to get the latest value of the value without recreating a new callback.

const valueRef = useRef(value);

useEffect(() => {
  // I don't care that it's executed at each render
  // because I want always the latest value
  // I save a check on the dependency
  valueRef.current = value;
});

const reallyStableCallback = useCallback(() => {
  console.log("I use the dep:", valueRef.current);
}, []);
Enter fullscreen mode Exit fullscreen mode

Warning: Do not forget to update the reference! Otherwise, you will not have the latest value.


Count the number of render

You can easily store the number of render thanks to a ref combined with useEffect:

function MyComponent() {
  const renderCount = useRef(1);

  useEffect(() => {
    renderCount.current++;
  });

  return <p>Number of render: {renderCount.current}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Know if a component has been already mounted

function MyComponent() {
  const isMounted = useRef(false);
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (isMounted.current) {
      console.log("The count has changed to:", count);
    }
  }, [count]);

  useEffect(() => {
    isMounted.current = true;
  }, []);

  return (
    <button
      type="button"
      onClick={() => setCount((prev) => prev + 1)}
    >
      Inc count: {count}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Warning: When using this pattern, to know if the component is mounted in a useEffect then the order of useEffect is really important. The one that updates the isMounted MUST be the last one.


Keep a previous value

Another use case is when you want to keep the value of a state during the previous render. It can be useful when you compare to the current one in a useEffect to know if it is one of the dependency that has changed.

function MyComponent() {
  const [otherState, setOtherState] = useState(0);
  const [count, setCount] = useState(0);
  const previousCount = useRef(count);

  useEffect(() => {
    if (previousCount.current !== count) {
      console.log(
        "The count has changed during this render " +
          "(maybe otherState too)"
      );
    } else {
      console.log(
        "It's sure that otherState has changed " +
          "during this render"
      );
    }
  }, [count, otherState]);

  useEffect(() => {
    previousCount.current = count;
  }, [count]);

  return (
    <div>
      <button
        type="button"
        onClick={() => setCount((prev) => prev + 1)}
      >
        Inc count: {count}
      </button>
      <button
        type="button"
        onClick={() => setOtherState((prev) => prev + 1)}
      >
        Inc otherState: {otherState}
      </button>
      <button
        type="button"
        onClick={() => {
          setCount((prev) => prev + 1);
          setOtherState((prev) => prev + 1);
        }}
      >
        Inc both
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: It's not because the count has changed during the render than the otherState did not changed too. It can happen because React batches update in that case.


How React assigns the DOM node to a ref?

Previously we have seen than the main use case is to get a reference to a DOM node. But how does React do it under the hood?

One thing you should understand is the difference of execution between useEffect and useLayoutEffect: layoutEffects are executed synchronously after the rendering phase contrary to effects that are executed asynchronously (they are just schedule but not ensure to be executed directly).

At the first rendering, React will transform React elements into Fiber nodes.

Basically, during the rendering, React will process from the Root node until the deepest component. Then it will go up in the component tree.

React rendering phase

Note: Here I have a simple tree with no siblings component. When there are siblings it processes a branch, complete the work and go on the other branch.

Begin work phase:

When processing a node, from top to bottom, React can detect when a node is a HostComponent (i.e. div, p, ... native DOM tag) and has a prop ref assign to it.

If it's the case, React will flag this node and put on the fiber node a ref key containing the reference to the ref (which is basically an object with a current key as we have seen earlier).

Complete work phase:

Then, when React has reached the last child it will go up in the tree, it's at this moment that the previously setted flag has an effect. It will tell to the parent fiber node:

HostComponent: "Hey, someone wants my DOM node, put me in you (fiber node) as a firstEffect that will be executed before all layoutEffect you have :)"

Then the parent fiber node tells to its parent:

HostComponent parent: "Hey, we told me that this fiber node is my firstEffect it needs to be yours too. But I have some layoutEffect can you execute me next, please?"

And this discussion happens to each fiber node until we get back to the Root fiber node.

Then the Root fiber node has just to execute its firstEffect.

This effect in our case, will be the one that has the ref flag that has already used previously. Because React detects the flag it will then attach the DOM node into the ref if it's an object of pass it as a parameter if it's a function (see callback ref in the previous part).

I want to make an article dedicated to how works React under the hood, hoping you will enjoyed it. If it's case do not hesitate to tell me in comments to give me motivation <3

Conclusion

React ref has multiple use cases that we have seen previously, do not hesitate to tell when you are using them.
The things you need to keep in mind:

  • changing a ref will no trigger a re-render
  • do not update / read a ref directly in render but in useEffect / useLayoutEffect and event handlers. Except when doing lazily initialization.
  • do not overused React state when in fact you do not need to use the value for the UI.
  • when you use a ref to prevent putting a dependency on useEffect / useLayoutEffect or useCallback that should not trigger the execution of the effect / re-creation of the callback. Do not forget to update in a useEffect / useLayoutEffect. In a next article, we will see that refs are also useful to use the native hook named useImperativeHandle.

Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.

Top comments (4)

Collapse
 
hayk1997 profile image
Hayk-1997

This was perfect article 👍👍

Collapse
 
romaintrotard profile image
Romain Trotard

Oh thank you so much :*

Collapse
 
ziwei profile image
徐紫微

Thers was a mistake in the article.

function MyComponent() {
const renderCount = useRef(1);

useEffect(() => {
renderCount.current++;
});

return

Number of render: {renderCount.current}

;

}
Collapse
 
romaintrotard profile image
Romain Trotard

Oh thanks. I fixed it :)