Go to main content

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.


A React reference is simply an object that has its reference which is fixed for all component renders and 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.


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

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

import React from 'react';

const ref = React.createRef();

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>;
  }
}
  • 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>;
  }
}

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>;
  }
}

After 16.8.6, hooks have been introduced, especially useRef:

import { useRef } from 'react';

const ref = useRef(initValue);

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>;
}

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';

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 no 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
  );
}

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 inconsistency if a ref is used inside render for UI (because we mutate an object). Whereas React will ensure there is no inconsistency 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.


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.

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} />;
}

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>
  );
}
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>
  );
}
// Forward the ref
const FunctionalComponent = React.forwardRef(
  (props, ref) => {
    // Content of component
  }
);

// Different name
function FunctionalComponent({ customRef }) {
  // Content of component
}

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>
  );
}

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]);

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);
}, []);

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}</p>;
}

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>
  );
}

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>
  );
}

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

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 previous 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 enjoy it. If it's case do not hesitate to tell me on Twitter to give me motivation <3

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 overuse 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.

You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.