engineering

useRef Hook in React - Use Cases, Code Examples, Interview Prep

useRef Hook in React - Use Cases, Code Examples, Interview Prep

The best guide on the useRef hook you'll find on the internet. Learn about its use cases with code examples, theory deep dives, caveats, and interview prep

Kaushal Joshi

Kaushal Joshi

Dec 13, 2024 12 min read


The useRef hook provided by React.js is often a neglected React hook for interview preparation. Yet, interviewers focus on it as much as they do on other core React concepts. The useRef can be challenging to understand because it behaves differently from other hooks. It bypasses some of React's core rules, making it more confusing to understand.

In this article, we will dive deep into React’s useRef hook. We will start from the basics and understand how it works, how to use it, and how to avoid common pitfalls. Then we’ll see how useRef differs from useState and createRef. Finally, we’ll end with useRef interview questions to revise our learnings.

Prerequisites

This article presumes that you have a basic understanding of React.js. This includes states, JSX, component-driven architecture, functional components, etc. If you don’t, I’d recommend learning React first. react.dev/learn is a good place to start your React journey.

What is useRef?

useRef is a built-in React hook that is used to store values across renders. The value stored with the useRef hook persists even if a component rerenders. Similarly, if the value inside the useRef object gets updated, the component does not re-render.

Here’s the basic syntax of useRef hook:

const ref = React.useRef(initialValue);

Parameters of useRef hook

  • initialValue: This is the initial value you want to have for the ref object's current property. It is ignored after the initial render when the current property is updated for the first time.

Return values of useRef hook

  • ref: useRef returns an object with a single property: current. During the initial render, it is set to initialValue. You can later set it to something else. On future renders, it returns the same object as the previous render.

Use cases for useRef hook

If you have never used this hook before, I know you are confused. Let’s understand why would you want to store a value across renders, or update it without triggering a component re-render.

Referencing a value with useRef

The most basic use case of the useRef hook is to reference a value across rerenders. Let me explain.

Ideally, we use React’s useState hook to store values. But updating a state causes React to re-render. Sometimes, we don’t want that. Consider scenarios like tracking data that won’t be shown on the UI — like timers, counters, previous states, etc. We want to update them without triggering the rerender.

That’s when we prefer useRef over other hooks. It is used when we want to do the following:

  1. Update a value without causing a component to rerender.

  2. Access a particular value even if a component rerenders.

Here’s a simple example of the useRef hook:

import { useRef, useEffect } from "react";

export default function Counter() {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`Component has rendered ${renderCount.current} times.`);
  });

  return <div>Check the console for render count.</div>;
}

In this example, renderCount will increase with each render, but changing its value doesn’t cause a rerender. Similarly, the data persists even if the component rerenders.

Manipulating the DOM with useRef

Another common use case is to access DOM directly. Traditionally, React discourages accessing DOM directly from within the component. But sometimes you need to access DOM directly anyway. Consider scenarios like:

  1. Managing focus and selection of HTML elements

  2. Measuring or Modifying Element Dimensions

  3. Controlling Animations or Transitions

  4. Interacting with Third-Party Libraries

You can pass the return value of the useRef hook to the ref attribute of the HTML node you want to manipulate.

import { useRef } from "react";

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

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click button to focus" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

Here, inputRef is used to directly access the <input> DOM element and focus on it when the button is clicked.

Handling Intervals and Timeouts

Another common use case for the useRef hook is to handle intervals or timeouts. Since useRef can persist a value across renders, it’s perfect for holding references to timers without causing issues with rerenders.

import { useRef, useEffect, useState } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = window.setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
    };
  }, []);

  return <div>Seconds passed: {seconds}</div>;
}

In this example, timerRef holds the interval ID and persists across renders. When the component unmounts, we can use timerRef.current to clear the interval.

Avoiding Common Pitfalls and Errors While Using useRef hook

Now let’s look at some common errors you might encounter while using the useRef hook.

Cannot read properties of null (reading 'useRef')

This error typically occurs when you're trying to interact with a ref before it is properly initialized.

To solve this, always ensure the ref is properly assigned before accessing it. For instance, in the input focus example, inputRef.current could be null initially, so you must ensure it's mounted before calling methods on it.

if (inputRef.current) {
  inputRef.current.focus();
}

useRef not updating and rendering new values

If you’re reading this article carefully, you must have understood why this happens and how you could fix it.

Let’s look at an example.

import { useRef } from "react";

function RefExample() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;
    console.log("Ref value:", countRef.current); // Logs the updated value
  };

  return (
    <>
      <p>Current ref value: {countRef.current}</p> {/* This will NOT update */}
      <button onClick={handleClick}>Increment Ref</button>
    </>
  );
}

In the code above, the UI will NOT update when you click the button, even though countRef.current is changing. The useRef does not update the UI when the value current is changed because it doesn’t cause rerender.

If you need the UI to reflect changes, you should use useState instead of useRef. Here’s how the same example would look with useState:

import { useState } from "react";

function StateExample() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1); // This will trigger a re-render
  };

  return (
    <>
      <p>Current count: {count}</p> {/* The UI will update */}
      <button onClick={handleClick}>Increment State</button>
    </>
  );
}

Always remember, useRef is designed to store values without causing rerenders. For any reactive changes to the UI, you should use useState.

useRef vs useState hook — Which One to Use?

React also provides another hook to store data, called useRef hook. We just saw an example in the previous section. You must be thinking why use a confusing hook like useRef when useState exists? Let’s find it out.

The useState hook is used when we must update the content on the screen if the state’s value changes. By default, React rerenders a component, along with its child component if the state updates. Similarly, when a component rerenders, the state is also updated with the latest value.

The useRef hook, on the other hand, does not trigger a rerender. Hence we can persist a value even if a component rerenders, or update a value without causing a component to rerender.

useRef vs createRef — What’s the Difference?

If you have been using React for quite a long, or have referred to an older video or documentation, you must have come across createRef. The main difference between useRef and createRef is that the first is used in functional components whereas the latter is used in class components.

The key difference is that createRef always creates a new reference every render, while useRef maintains the same reference across renders.

Here’s the basic syntax of createRef():

class MyComponent extends React.Component {
  inputRef = React.createRef();

  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return <input ref={this.inputRef} />;
  }
}

If you are working with functional components, you should always prefer useRef and avoid createRef.

The official documentation for createRef

TypeScript with useRef Hook

When using useRef in TypeScript, it's important to define the correct types for the values you're storing, especially if you're referencing DOM elements.

Here's how you can use useRef with TypeScript to reference DOM elements:

import { useRef, useEffect } from "react";

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null); // HTMLInputElement type

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus(); // Focus the input when the component mounts
    }
  }, []);

  return <input ref={inputRef} type="text" placeholder="Focus on me!" />;
}

Here, useRef<HTMLInputElement>(null) creates a ref for an <input> element. The null is the initial value because the DOM element is not available until after the component has been rendered. You can get the type of a JSX node by simply hovering over it, over searching it on Google or ChatGPT :P

Why Do We Need to Store Values Across Renders?

Let’s say you have a timer that you want to increment every second. If you store the timer in a regular variable, the timer will be reset each time the component re-renders.

let timer = 0;
useEffect(() => {
  const interval = setInterval(() => {
    timer += 1;
    console.log(timer); // Timer resets on every re-render
  }, 1000);

  return () => clearInterval(interval);
}, []);

In React, every time a component rerenders, it runs the entire function again. During each render, all variables and values inside the function are re-initialized. This means that any value stored in regular variables will be reset after every render.

To persist values between renders we need a way to store them that won’t be reset when the component rerenders.

const timerRef = useRef(0); // Use useRef to persist the timer value across renders

useEffect(() => {
  const interval = setInterval(() => {
    timerRef.current += 1; // Update the ref value
    console.log(timerRef.current); // Logs the updated timer value
  }, 1000);

  return () => clearInterval(interval); // Clean up the interval on unmount
}, []);

This is the correct version of the previous code. It persists the interval even if the component rerenders.

Why Can't We Use the document Object to Access the DOM Directly?

While you can technically use the document object to access and manipulate the DOM directly, it’s generally discouraged in React for several reasons:

  1. React’s Virtual DOM: React uses a virtual DOM to manage changes to the real DOM efficiently. When you directly manipulate the real DOM with document.getElementById or document.querySelector, you’re bypassing React’s virtual DOM system. This can lead to inconsistencies between what React thinks is rendered and what’s actually in the DOM, causing bugs and unexpected behavior.

  2. Component Reusability and Isolation: React encourages a component-based architecture. By directly manipulating the DOM outside of React, you break the isolation of your components. This reduces maintainability and testability, as components are no longer self-contained.

  3. Re-renders: React can re-render components at any time based on state or props changes. If you manipulate the DOM directly, your changes can be overwritten by React when it re-renders a component. Using refs ensures that React’s re-renders do not interfere with your DOM manipulations.

Instead of using document, React recommends using useRef to interact with DOM elements while still adhering to React’s virtual DOM principles.

Why Does useRef Have a current Property, and Why Can't We Store Data Directly?

The useRef hook returns an object with a current property because it needs to store mutable values in a way that React can manage efficiently, without causing re-renders.

Why the current Property?

  • Consistency: The current property is always there. It provides a consistent interface, whether you are storing a DOM node reference or any mutable value. This makes useRef more predictable, regardless of what kind of value you're holding in it.

  • Rerender Management: The value stored in ref.current is outside of React’s state management and re-render cycle. If you were to store the value directly in the ref object itself (rather than ref.current), there would be no way for React to isolate the value across re-renders. Using ref.current allows React to maintain this separation and optimize performance.

Why Can’t We Store Data Directly?

  • Mutable Object: useRef is designed to hold mutable values, and this mutability needs to be managed carefully. If React allowed data to be stored directly in the object, the reference could be reassigned completely, potentially causing bugs or confusion. The current property provides a controlled way to manage this mutability without accidentally reassigning the reference itself.

  • Uniform Interface: Whether you're referencing a DOM node or storing a custom value, the uniformity of using ref.current makes useRef versatile and avoids confusion. The reference (ref) itself should not change between renders, but its current value can.

In short, the current property ensures that you always access or modify the intended value, and React maintains control over the reference lifecycle.

Recap — useRef hook, In a Nutshell

Let’s take a deep breath and recap everything we learned today.

The useRef hook in React allows you to store persistent values across component renders. These values could be valid JavaScript primitive or non-primitive types or HTML DOM nodes. The hook takes an initial value as a parameter and returns an object with a single property called current, which can be updated without triggering a rerender of the component.

You must use the useRef hook if you want to —

  1. Update a value without causing a component to rerender

  2. Access a particular value even if a component rerenders

Common use-cases:

  1. Referencing a value with useRef

  2. Manipulating the DOM with useRef

  3. Handling Intervals and Timeouts

Interview Preparation

If you're preparing for frontend interviews, the following list of questions would help you prepare for the useRef hook in depth:

  1. What are the common pitfalls you might encounter while using the useRef hook?

  2. What’s the difference between the useState and the useRef hook? When should you use which?

  3. What’s createRef, and how does it defer from useRef?

  4. Why does useRef have a current property, and why can't we store data directly?

  5. Why can't we use the document object to access the DOM directly?

Wrapping Up

Thank you for sticking through until the end! I hope you found this article helpful and learned new concepts about the useRef hook. If you did, you must share this article with your friends and peers! After all, helping your peers to grow is such a wonderful feeling.

On a side note, as you read this blog, I can’t help but think you’re preparing for job interviews! If that’s the case, don’t forget to check out Peerlist! Whether you’re searching for jobs, looking for a way to share your side projects, or building a credible proof of work, Peerlist is the go-to stop for it!

Until then, happy coding! May your skills persist across different domains 🪄

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.