DEV Community

Cover image for The React useRef Hook: Not Just for DOM Elements
Nick Taylor for OpenSauced

Posted on • Updated on • Originally published at opensauced.pizza

The React useRef Hook: Not Just for DOM Elements

In this post, we'll cover what the useRef hook is, some examples of how it can be used, and when it shouldn't be used.

What is useRef?

The useRef hook creates a reference object that holds a mutable value, stored in its current property. This value can be anything from a DOM element to a plain object. Unlike component state via say the useState hook, changes to a reference object via useRef won't trigger a re-render of your component, improving performance.

Examples

Referencing a DOM element using the useRef Hook

In React, state manages data that can trigger re-renders. But what if you need a way to directly access document object model (DOM) elements that shouldn't cause re-renders? That's where the useRef hook comes in.

Typically, you'd do something like this.

import { useEffect, useRef } from "react";

export const SomeComponent = () => {
  const firstNameInputRef = useRef<HTMLInputElement>(null);

  // for plain JavaScript change the above line to
  // const firstNameInputRef = useRef(null);

  useEffect(() => {
    firstNameInputRef.current?.focus();
  }, [firstNameInputRef.current]);

  return (
    <form>
      <label>
        First Name:
        <input type="text" ref={firstNameInputRef}/>
      </label>
    </form>
  );
}
  1. We create a variable named firstNameInputRef using useRef to reference the DOM element (initially null) and use useEffect to focus the input element on the initial render.
  2. Inside useEffect, we check if firstNameInputRef.current exists (it will be the actual DOM element after the initial render). If it does, we call focus() to set focus on the input.
  3. The dependency array [firstNameInputRef.current] ensures useEffect only runs once when the reference changes (i.e., after the initial render).

Referencing a non-DOM element using the useRef Hook

Recently, I was working on Open Sauced's StarSearch, a Copilot for git history feature we released at the end of May 2024. You can read more about StarSearch in the blog post below.

The ask was to be able to start a new StarSearch conversation. To do so, I had to stop the current conversation. If you've worked with the OpenAI API or similar APIs, they typically return a ReadableStream as a response.

A ReadableStream is a web API that allows data to be read in chunks as it becomes available, enabling efficient processing of large or real-time data sets. In the context of API responses, this means we can start handling the data immediately, without waiting for the entire response to complete.

I initially had this feature working, but ran into issues if the response started to stream. The solution, create a reference to the readable stream via the useRef hook and when a new conversation is started, cancel the one in progress. You can see these changes in the pull request (PR) below

fix: now a new StarSearch chat can be started if one was in progress #3637

Description

Now isRunning is reset to false when starting a new conversation. This was preventing the stream conversation from beginning when a previous one was cancelled and a new conversation started.

Related Tickets & Documents

Fixes #3636

Mobile & Desktop Screenshots/Recordings

Before

CleanShot 2024-06-25 at 21 25 35

After

CleanShot 2024-06-25 at 21 26 04

Steps to QA

  1. Go to any workspace and open StarSearch
  2. Start a conversation
  3. Cancel it by clicking the back button or new conversation buttons in the compact StarSearch header.
  4. Start the new conversation.
  5. Notice the new conversation streams in.

Tier (staff will fill in)

  • [ ] Tier 1
  • [ ] Tier 2
  • [ ] Tier 3
  • [x] Tier 4

[optional] What gif best describes this PR or how it makes you feel?

So now, if someone presses the Create a New Conversation button, I cancel the current streaming response from StarSearch, e.g.

  const streamRef = useRef<ReadableStreamDefaultReader<string>>();

  // for plain JavaScript change the above line to
  // const streamRef = useRef();  
...

  const onNewChat = () => {
    streamRef.current?.cancel();
    ...
  };

...

  1. We create a variable named streamRef using useRef to hold a reference to the current ReadableStreamDefaultReader.
  2. The onNewChat function checks if streamRef.current exists (meaning a stream is ongoing).
  3. If a stream exists, we call cancel() on streamRef.current to stop it before starting a new conversation.

Wrapping Up

useRef was the perfect solution for my use case. Maybe you'll find the useRef hook useful for something other than referencing a DOM element as well.

You can store almost anything in a reference object via the useRef hook, and it won't cause re-renders in your component. If you're persisting component state, opt for useState or other hooks like useReducer so that the component does re-render.

For further reading on the useRef hook, I highly recommend checking out the React documentation for the useRef hook.

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Top comments (15)

 
nickytonline profile image
Nick Taylor • Edited

What are some other use cases you've used the useRef hook for?

Thinking emojis floating around

 
giovannimazzuoccolo profile image
Giovanni Mazzuoccolo

Besides accessing DOM elements, I've primarily used useRef for handling websockets (similar to your case), managing formData values, SVG drawing, and integrating third-party APIs that weren't designed for use with React.

 
nickytonline profile image
Nick Taylor • Edited

Nice! Thanks for sharing, Giovanni.

 
ashishsimplecoder profile image
Ashish Prajapati

When you want track any kind of value and don't want to trigger rerender of components when that value is updated, that's the perfect use case of useRef.
In simple words when don't want to link the state to the UI.

 
nickytonline profile image
Nick Taylor

Jack Nicholson nodding yes

 
syeo66 profile image
Red Ochsenbein (he/him)

Basically for any kind of value which should be be stored across but should not trigger a rerender.

 
nickytonline profile image
Nick Taylor

Pretty much that. Thanks for giving it a read Red!

 
ashishsimplecoder profile image
Ashish Prajapati

Have used it to replace useMemo and useCallback.

 
link2twenty profile image
Andrew Bone

Great place to store an abort controller that can be accessed by several functions.

 
nickytonline profile image
Nick Taylor

Yeah, that's another great example @link2twenty! Do have any example code lying around to share in a gist or CodeSandbox?

 
link2twenty profile image
Andrew Bone • Edited

Here is a quick demo I just threw together. AbortController's are super good at preventing your frontend app spamming endpoints.

codesandbox.io/p/sandbox/abort-con...

import { useRef, useState } from "react";
import "./styles.css";

const dateFormat = Intl.DateTimeFormat("en-GB", {
  year: "numeric",
  month: "numeric",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  hour12: false,
  timeZone: "Europe/London",
});

export default function App() {
  const [lastUpdated, setLastUpdated] = useState<number>(Date.now());
  const [buttonPresses, setButtonPresses] = useState<number>(0);
  const [completedRequest, setCompletedRequest] = useState<number>(0);

  const controller = useRef<AbortController | null>(null);

  /**
   * load a mock slow endpoint
   */
  const getData = async () => {
    // abort any outstanding requests
    controller.current?.abort();
    controller.current = new AbortController();

    setButtonPresses((n) => n + 1);

    try {
      await fetch("https://hub.dummyapis.com/delay?seconds=5", {
        signal: controller.current?.signal,
      });

      setCompletedRequest((n) => n + 1);
      setLastUpdated(Date.now());
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <div className="App">
      <p>
        I last got data from the api {dateFormat.format(new Date(lastUpdated))}
        <br />
        You have pressed the 'update now' button {buttonPresses} time
        {buttonPresses !== 1 ? "s" : ""}
        <br />I have visited the endpoint {completedRequest} time
        {completedRequest !== 1 ? "s" : ""}
      </p>
      <button onClick={getData}>Update now</button>
      <button onClick={() => controller.current?.abort()}>
        Cancel request
      </button>
    </div>
  );
}
Thread
 
nickytonline profile image
Nick Taylor

Thanks for sharing!

Yes, that's awesome!

 
praveen_kumargovindaraj_ profile image
Praveen Kumar Govindaraj

Informative

 
nickytonline profile image
Nick Taylor

Glad you found it informative! The More You Know NBC TV campaign video capture

 
shaogat_alam_1e055e90254d profile image
Comment hidden by post author - thread only accessible via permalink
Shaogat Alam

Interesting topic! Everything is explained articulately and clearly. For your project, consider checking out this free npm package: select-paginated.

Some comments have been hidden by the post's author - find out more