A Complete Guide to useRef

What's useRef

useRef allows you to keep a mutable value within a component, similar to useState or instance variables on a class, without triggering re-renders.

For example, this component stores the number of clicks for a button:

function RefButton () {
  const clicks = useRef(0)

  return (
    <button onClick={() => (clicks.current += 1)}>
      Clicks: {clicks.current}
    </button>
  )
}

This is how this component looks like (I added a re-render button so you can actually test it out ๐Ÿ˜„):

Interactive Example

The example below is completely interactive, try clicking the "Clicks" button and then click on "Re-render".

As you can see, if you click the "Clicks" button it doesn't do anything. However, after click on "Re-render", it gets updated with the number of clicks we did previously.

Difference with a variable

You might wonder why not just use a simple variable as the example below:

let clicks = 0;

function OutsideVariableButton() {
  return (
    <button onClick={() => (clicks += 1)}>
      Clicks: {clicks}
    </button>
  )
}

And here's an interactive example for it:

Interactive Example

TypeResultoutside variable

The button works the same way that our previous example. However, the problem arises when you have multiple instances of the same component like the example below. Try clicking just one of the buttons and then click on re-render to see the result.

Interactive Example

TypeResultoutside variableoutside variableoutside variable

As you were able to see, the clicks are not isolated. In fact, all the examples from this article uses the same button component, so if you click the button from the first example and then click on "re-render" on the second example, the count it is gonna be incremented! What a bug ๐Ÿ›.

On the other hand, useRef values are completely isolated between components:

Interactive Example

TypeResultrefrefref

Difference withย useState

The main difference between useState and useRef, is that useState triggers a re-render and useRef doesn't.

In the following example I added two buttons: one that updates its count with useRef and the other one with useState. I added some labels so you can identify them easily.

Interactive Example

TypeResultstateref

You'll notice that clicking on the button with useRef doesn't trigger a re-render and thus, the view isn't updated. On the other side, when you click on the button that uses useState, it will update its clicks count immediately.

Refs as a Way to Access Underlying DOM Elements

To perform imperative actions on DOM nodes, React provides a way to get a reference to them via refs. All you have to do is to assign a ref property to a node with a ref object like this:

function CustomInput() {
  const inputRef = useRef()

  return <input ref={inputRef} />
}

The way to get a DOM reference using refs works (informally ๐Ÿ˜…) as follows:

Today
Hey React12:00
React
Hey, what's up?12:00
Could you give me a reference to this dom node?12:00
React
Sure, I assigned it to the 'current' property of your ref.12:00
Cool, thanks!12:00

On the first render, inputRef's value will be { current: null } and in the following renders it will have its current property assigned to the specified DOM node:

// First render: { current: undefined }
// Second render: { current: <input /> }
// Third render: { current: <input /> }
/// ...and so on
console.log(inputRef)

However, if you only reference inputRef inside useEffect then it'll always reference the DOM node so you don't need to worry about it being undefined.

Let's update our example to get an idea of how this works:

function AttachingToDomExample() {
  const inputRef = useRef()

  console.log("Render inputRef value:", inputRef)

  useEffect(() => console.log("useEffect inputRef value:", inputRef))

  return <input ref={inputRef} />
}

Here's the console output when rendering this component:

RenderLocationValue
1Render{ current: undefined }
useEffect{ current: <input /> }
2Render{ current: <input /> }
useEffect{ current: <input /> }
3Render{ current: <input /> }
useEffect{ current: <input /> }

As you can see, if you access the inputRef inside useEffect then you don't need to worry about it being undefined because React will assign it automatically for you.

Real World Use Cases for Refs

Let's start with a simple real-world application for refs: usePrevious. This hook stores the previous value for a given state variable. It is even referenced on React's docs as a way to "get the previous props or state". Let's see it in action first:

function UsePreviousExample() {
  const [clicks, setClicks] = useState(0)
  // usePrevious is the important part here โœจ
  const previousClicks = usePrevious(clicks)

  return (
    <div>
      <button onClick={() => setClicks(clicks + 1)}>
        Clicks: {clicks} - Before: {previousClicks}
      </button>
    </div>
  )
}

Here's the output so you can play with it:

Interactive Example

You can notice that the previousClicks variable stores the value for the previous render for a given variable. Here's its implementation:

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  }, [value])
  return ref.current
}

Let's analyze how it works.

Let's simulate what happens on the first render. We can remove the call to useEffect since it doesn't affect the return value on the first render:

// First render
function usePrevious(value) {
  const ref = useRef()
  return ref.current
}

On the first render it is called with a value of 0:

// First render
const previousClicks = usePrevious(0)

In this case, usePrevious will return undefined:

// First render
function usePrevious(value) {
  const ref = useRef()
  return ref.current // No assignment, so it's undefined
}

After increase the value for count, here's how the usePrevious call will look:

// Second render
const previousClicks = usePrevious(1)

Since usePrevious is called again, its effect needs to run:

useEffect(() => {
  ref.current = 0
})

After this, the usePrevious function is called again:

// First render
function usePrevious(value) {
  const ref = useRef()
  return ref.current // 0
}

And so on. Here's the value for each render for both variables:

RenderclickspreviousClicks
10undefined
210
321
432

Callback Refs

Callback Refs are a different way to set refs. It gives you a fine-grain control over when refs are attached and detached because you provide a function instead of a ref variable. This function gets called every time the component mounts and unmounts.

Here's an example that shows/hides an emoji every time you click its button. The important thing here is the ref prop that we added. We use a function to log the provided ref:

const callback = (ref) => console.log("callback:", ref)

function App () {
  const [show, setShow] = useState(true);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        {show ? "Hide" : "Show"}
      </button>
      {show && <span ref={callback}>๐Ÿ‘‹</span>}
    </div>
  );
}

Here's an interactive version of the previous code (you can check the output in the console to see that I'm not lying ๐Ÿ™ƒ):

Interactive Example

๐Ÿ‘‹
>> ref: "<span>๐Ÿ‘‹</span>"
>>

Note: If you use callback refs as inline functions, it will be called twice: one with null and another one with the DOM element. This is because React needs to clear the previous ref every time the function is created. A workaround for this is to use a class method.

(Legacy) String Refs

Warning

String refs are a legacy feature and they are likely to be removed in future React versions.

The way it works is that you provide a string as a ref value like ref="exampleRef" and it automatically gets assigned to this.refs.

Note: String refs can only be used with class components.

Here's an usage example:

export default class App extends React.Component {
  render() {
    console.log(this.refs);

    return (
      <div ref="exampleRef">
        <button onClick={() => this.setState({ dummy: 0 })}>Re-render</button>
      </div>
    );
  }
}

Here's the value for this.refs across renders:

Renderthis.refs
1{}
2{ exampleRef: <div>...</div> }
3{ exampleRef: <div>...</div> }
4{ exampleRef: <div>...</div> }

As you can see, on the first render this.refs.exampleRef will be undefined and on the following renders it will point out to the specified DOM node.

Conclusion

We saw what useRef is, how it differentiates with a plain old variable and state variables, and we saw real world examples that uses it. I hope that most of the content makes sense to you!

I'd love to hear your feedback. You can reach out to me on Twitter at any time :-)


Written by Giovanni Benussi a frontend developer interested in React, Open Source, and writing good plain HTML and CSS. You can follow him on Twitter.

ยฉ 2021, Built with Gatsby