320H.8 - React Hooks: useRef

Learning Objectives

By the end of this lesson, learners will be able to:

  • Describe the difference between useRef and useState.
  • Use useRef to store references across renders.
  • Create and use ref callback functions.
  • Use useRef to reference and manipulate DOM elements.
  • Use forwardRefs to add refs to custom components.
  • Use the useImperativeHandle hook to control which DOM methods are exposed to custom refs.
  • Create uncontrolled forms using refs.

 CodeSandbox

This lesson uses CodeSandbox as one of its tools.

If you are unfamiliar with CodeSandbox, or need a refresher, please visit our reference page on CodeSandbox for instructions on:

  • Creating an Account
  • Making a Sandbox
  • Navigating your Sandbox
  • Submitting a link to your Sandbox to Canvas

React "Escape Hatches"

It is not uncommon for components to control and synchronize with systems outside of React, such as focusing an input using the browser API, playing or pausing videos in a video player not implemented with React, or connecting and listening to message from a remote server.

React provides several tools it refers to as "escape hatches" that let you connect React to external systems, but the majority of your application logic and data flow should not rely on these systems.

The escape hatch that we will discuss during this lesson is the useRef hook.

When you want a component to remember information, but you don't want that information to trigger new renders (like with useState), you can use a ref (short for reference). Like state, refs are retained by React between renders. Unlike state, changing a ref does not trigger a new render.


The useRef hook

Before we can use useRef, we must import it:

import { useRef } from 'react';

The most basic call to useRef looks like the following. We pass the initial value that we want to reference as the only argument:

const ref = useRef(0);

We can then access the current value of the ref through the ref.current property, since useRef will return an object that looks like this:

{
  current: 0
}

This ref value is intentionally mutable, so that we can both read from it and write to it. These "escape hatches" like useRef are small sections of your code that React doesn't keep track of, which allows them to break a few of the rules that we've discussed so far.

The React documentation provides us with a very simple example of useRef in action. In the following code, we create a button that alerts us to how many times we've clicked it using the value of ref. This value isn't state, so there's no reason to re-render the application, but it is something we want to keep track of across renders.

If we didn't have the useRef hook, and instead simply set ref = { current: 0 }, it would be reset with every render of the application.

While this ref points to a number, you can point to any type of value: strings, objects, arrays, functions. Ref is just a plain JavaScript object with the current property that is both readable and modifiable.

Let's look at an example of a stopwatch built by the React team.

In this example, we display how much time has passed since the user clicked the "Start" button. In order to calculate this information, we will need to pieces of state: the time when the button was pressed (startTime), and the current time (now). This is state information, not ref information, because it is used for rendering the "time passed" display.

The example uses setInterval() to then update the now state information every 10 milliseconds.

It isn't much of a stopwatch without a "stop," is it?

In order to add a "Stop" button, we need to cancel the existing setInterval so that it stops updating the now state. We can do this by calling clearInterval, but in order to do so we need to pass it a reference to the interval we want to clear. Let's try this:

If we try stopping the stopwatch, it... doesn't work. If you check the console, you'll notice it is logging undefined for the value of intervalRef.current within handleStop().

This is because, as mentioned before, regular variables don't persist across renders. During one render, we set intervalRef.current to the setInterval reference, but during the next render it gets reset by const intervalRef = {}, resulting in an undefined value for current.

Change the definition of intervalRef to use useRef with an initial value of null and test the code again!

Here's a general rule of thumb when deciding between state data and reference data:

  • If a piece of information is used for rendering, keep it in state.
  • If changing a piece of information doesn't require a re-render, using a ref may be more efficient.

Refs are typically used within event handlers.


Contrasting Refs and State

In most cases, using state is the correct course of action. While refs seem less strict than state due to their mutability, state provides the bread and butter of React applications. Refs are, as described before, an "escape hatch" that shouldn't be overused.

Here's a comparison between refs and state provided by the React team:

refs state
useRef(initialValue) returns { current: initialValue } useState(initialValue) returns the current value of a state variable and a state setter function ([value, setValue])
Doesn’t trigger re-render when you change it. Triggers re-render when you change it.
Mutable — you can modify and update current’s value outside of the rendering process. “Immutable” — you must use the state setting function to modify state variables to queue a re-render.
You shouldn’t read (or write) the current value during rendering. You can read state at any time. However, each render has its own snapshot of state which does not change.

Of the table above, one of the most important things to remember is that reading ref.current during render can lead to inaccurate results and unreliable code. If you need a value during render, use state instead.


Use Cases for Refs

Refs are most commonly used in conjunction with external APIs, like browser APIs that won't impact the appearance of a component.

We've already seen an example of this with storing setTimeout IDs, but you can also use refs to store and manipulate DOM elements or other objects that aren't necessary for calculating the render state.

We will discuss manipulating the DOM with refs shortly.


Ref Best Practices

If you think about refs as simple variables that persist across renders, that will guide you in the correct direction for using them properly. Refs aren't state information, they don't store what your application should look like, they're just basic JavaScript objects that don't lose their values on re-render.

Treat them as an escape hatch. They are useful for working with external systems or browser APIs, but they shouldn't be used as the primary driver for a React application. If you find that most of your data flow and application logic uses refs, you may want to consider a different approach.

Also, don't read or write to refs during rendering. If the information is required for rendering, it should be a piece of state. React doesn't know when refs change, so reading from refs during the rendering process makes behavior difficult to predict and control.

Since the limitations of typical React tools don't apply to refs, you don't need to worry about things like avoiding mutation when working with refs. As long as the object you are mutating isn't used during renders, React doesn't care.


DOM Manipulations with Refs

While refs can point to any kind of value, the most common use case is accessing DOM elements. When you pass ref as a prop to a JSX element in react, React puts the cooresponding DOM element into myRef.current!

This behavior enables a wide variety of options, but some of the most common ones are:

  • Focusing a node.
  • Scrolling to nodes.
  • Measuring node size or position.

While React handles the vast majority of DOM building and manipulation for us, sometimes components and event handlers need to handle some of these small tasks manually. This is where refs come into play.

Think of the useRef hook kind of like document.querySelector: it allows you to assign a DOM node to a variable so you can access its properties. React's declarative nature (express what you want, not how to make it) makes it hard to write normal imperative (how to make the thing step by step) DOM code.

Here are a couple of examples of the common uses for refs:

In the example above, the following code sets the <input> DOM element created by the JSX and React to the variable reference inputRef:

const inputRef = useRef(null);
// ...
<input ref={inputRef} />

From there, any DOM methods can be called on the <input> node being stored in inputRef. In the case of the example above, we call focus() within the button element's onClick handler, handleClick.

Here's another common usage provided by the React documentation - scrolling:

This example uses multiple refs to create a carousel of three images, with buttons that use the DOM's scrollIntoView method to bring each picture into view when its cooresponding button is pressed.


Ref Callbacks

It is unlikely that you will have a static, pre-defined number of elements in a use-case like the carousel example above. You can circumvent this issue by passing a function to the ref, called a ref callback.

React will call the ref callback with the associated DOM node when it is time to set the ref, which allows you to create and maintain your own array or map of refs, and access any ref by its index (or some other unique ID).

Below is an example that allows you to scroll to an arbitrary node within a longer list of unknown length. itemRef no longer holds a single DOM node, but rather a Map object that maps each item ID to a DOM node. The ref callback on every list item updates the Map, which lets us read individuals DOM nodes later.

Using these concepts, we can build some of the most commonly-seen and most popular React components on the web.

Here's a simple but adaptable Carousel component built using similar logic to the example above:

Note that this carousel is very simple and quite limited, but it serves its specific purpose. There are many other pre-built carousel components with customization options and a more robust structure available through a variety of channels.

During development, you will often have to decide between building your own custom components that are tailored to your application's needs, or using pre-packages components (and libraries) that accomplish the task for you, but might be bloated or not very performative for your purposes.


Accessing Other Component's DOM Nodes

It is important to note that you cannot use refs to reference custom components. This is an intentional restriction imposed by React, because allowing one component to access and manipulater another component's DOM nodes would result in extremely fragile code and many unintended behaviors.

Attempting to do so will give you an error that looks like this:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Note how this error gives the suggestion of using React.forwardRef(). The forwardRef method allows a component to opt into receiving the ref as its second argument from a component above it. That parent component can then pass a ref as an attribute/prop (though it is not stored in the props object), and the child can bind that ref to its own elements.

Here's a simple example showing how this works:

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

It is common for low-level components like buttons and inputs to forward their refs, but high-level components like forms, lists, page sections, etc. usually will not expose their DOM nodes, as doing so could create unintended dependancies and cause unwanted behavior.

This does lend itself to other issues, however. In the example above, we're allowing the Form component to do anything it wants with the DOM node exposed by MyInput.

In order to provide a more controlled experience, React gives us another hook: useImperativeHandle.

useImperativeHandle instructs React to creaate and provide a new special object as the ref value passed by forwardRef, instead of the complete DOM node. This new object can be created to only allow whichever methods you want the component to expose.

Here's the example above, modified with useImperativeHandle to only expose the focus() method:

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

export default function Form() {
  const inputRef = useRef(null);

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

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

Notice how we first created a "real" ref to the input element, and use that to populate the methods given to useImperativeHandle.

useImperativeHandle takes:

  • The ref being passed from forwardRef as its first argument.
  • A createHandle function that returns the ref handle you want to expose as the second argument. Usually, this returns an object with the methods that you would like to expose.
  • An optional dependancies list which contains a list of all reactive values listed within the createHandle code. These values can be things like state, props, or other variables being used to create the exposed methods. Typically, React will alert you to any dependancies that need to be within this list, if any. If any of these dependancies change, React will automatically trigger a re-render.

Refs within the Rendering Cycle

Refs are set during the commit phase of "trigger, render, commit." After updating the DOM, React sets each ref to its cooresponding DOM node.

Usually, you will access refs from within event handlers, but there are cases in which you want to do something with a ref outside of a particular event. This is where the useEffect hook comes into play, which we will discuss during the next lesson.

In general, you do not want to access refs during rendering, as they will likely be either inaccurate or null until the commit phase.


Ref DOM Manipulation Best Practices

Common usage of refs for DOM manipulation include things like managing element focus, scroll position, or calling APIs that React does not expose itself. Notice that these are all "non-destructive" actions - they don't modify the DOM, they only modify how it is viewed or interacted with.

If you try to directly modify the DOM with refs, you are likely to cause conflicts with the changes React makes itself. This can lead to unpredictable behavior and issues.

Here's an example from the React documentation that illustrates this problem:

"This example includes a welcome message and two buttons. The first button toggles its presence using conditional rendering and state, as you would usually do in React. The second button uses the remove() DOM API to forcefully remove it from the DOM outside of React’s control.

Try pressing “Toggle with setState” a few times. The message should disappear and appear again. Then press 'Remove from the DOM'. This will forcefully remove it. Finally, press 'Toggle with setState':"

"After you’ve manually removed the DOM element, trying to use setState to show it again will lead to a crash. This is because you’ve changed the DOM, and React doesn’t know how to continue managing it correctly."

These kinds of breaking DOM modifications could quickly snowball throughout a large, complex application, making it very difficult to find and debug the source of the issue. It is best to avoid changing the DOM altogether.

That said, you can safely modify parts of the DOM that React has no reason to update, as long as you do so with caution. An example of this is building an element that is typically empty, such as an empty div:

function MyComponent(props) {
  return <div id="myComponent"></div>;
}

If the <div> created by the JSX above never has any children managed by React, we can add to, remove from, or modify its children list without breaking any React behaviors.

Again, be cautious when attempting to do things like this, and always verify that what you are trying cannot be accomplished without using an "escape hatch" like refs. Often, there is a better, safer way.


Form Handling with Refs

There are two ways to handle forms in React, called controlled and uncontrolled forms:

  • Controlled Forms - The value of the inputs are bound to state, so the value of state and the value of the inputs are always in sync. A controlled form has:

    • Object holding form values as state.
    • A handleChange function that updates the state when the user changes form values.
    • A handleSubmit function to do what you want with the data when the form is submitted.
  • Uncontrolled Forms - The forms are not bound to state; instead, their values are pulled using a useRef when needed. An uncontrolled form, on the other hand, has:

    • A useRef created for each input.
    • A handleSubmit for when the form is submitted.

We have already seen a number of examples of controlled forms so far, but here is a reminder:

import { useState } from "react"

const Form = props => {
  // State to hold the form data.
  const [form, setForm] = useState({
    name: "",
    age: 0,
  })

  // handleChange function
  const handleChange = event => {
    // Dynamically update the state using the event object.
    // This function always looks the same.
    setForm({ ...form, [event.target.name]: event.target.value })
  }

  const handleSubmit = event => {
    // Prevent page refresh on submission.
    event.preventDefault()

    // Do what you want with the form data.
    console.log(form)
  }

  // The JSX for the form, binding the functions and state to our inputs.
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={form.name}
        onChange={handleChange}
        name="name"
        placeholder="Your Name"
      />
      <input
        type="number"
        value={form.age}
        onChange={handleChange}
        name="age"
        placeholder="Your Age"
      />
      <input type="submit" value="Submit Form" />
    </form>
  )
}

Controlled forms offer a variety of benefits, but they are not always necessary. As described at the beginning of this lesson, state is useful for holding data that affects renders, while refs hold data that does not.

A controlled form may provide its data as state to be rendered elsewhere, such as displaying the data for user re-verification or for using the data to change things like display theme.

If the form does not change any rendering properties, however, its data does not need to be stored in state.

Here's an example of the same form being handled by refs instead of being bound to state, known as an "uncontrolled" form:

import { useRef } from "react"

const Form = props => {
  // useRef to get input values.
  const nameInput = useRef(null)
  const ageInput = useRef(null)

  const handleSubmit = event => {
    // Prevent page refresh on submission.
    event.preventDefault()

    // Do what you want with the form data.
    console.log({
      name: nameInput.current.value,
      age: ageInput.current.value,
    })
  }

  // The JSX for the form, binding the functions and ref to our inputs.
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameInput} placeholder="write name here" />
      <input type="number" ref={ageInput} placeholder="write age here" />
      <input type="submit" value="Submit Form" />
    </form>
  )
}

Summary

The useRef hook is used to store references to data that persists between renders. The current value of a ref is stored in the ref.current property.

Most often, you'll use refs to hold DOM elements. You can instruct React to store a DOM node by passing ref={someRef} to the element you would like to store as a ref.

Custom components do not expose their DOM elements by default. In order to expose a custom component's DOM nodes, you'll need to use the forwardRef method when creating that component, which accepts ref as its second argument (and props as the first). You can also use the useImperativeHandle hook within the component to control which methods the ref exposes.

In general, you should avoid changing any DOM nodes that are managed by React. If you know that React has no reason to update specific nodes, you may cautiously modify them using refs, but be certain that you thoroughly test your code!

When determining whether to use refs or state, ask yourself if you want changes in the information to trigger re-renders. If the answer is yes, use state. Similarly, you can create controlled forms by binding their data to state, but only if you need that data to be state data. Otherwise, uncontrolled forms can be created using refs.


Practice Activity: Using Refs

The following CodeSandbox contains an example that uses a state variable isPlaying to switch between a "playing" and "paused" state, changing the rendered display of the button's text depending on the result.

Add a ref to the video element, and call its play() and pause() methods within the button's handleClick() function.

Afterwards, find a way to keep the isPlaying state in sync with the video's state even if the user right-clicks the video and plays it using the built-in browser controls. To do so, use setIsPlaying() with the onPlay and onPause video events.

Copyright © Per Scholas 2024