OWolf

2024-09-24 Web Development

Understanding useRef in React

By O. Wolfson

In React, managing component renders efficiently is crucial for building high-performing applications. A render in React refers to the process of updating the virtual DOM, a lightweight copy of the actual DOM, based on changes in data or state. The virtual DOM is innovative because it allows React to compare (or "diff") the old and new virtual DOM versions to identify exactly which parts of the real DOM need to be updated. This makes React more efficient than directly manipulating the DOM, which can be slow and costly. While React optimizes updates, minimizing unnecessary renders is key to boosting performance, as rendering too often can lead to wasted processing power and slower UI updates.

This article focuses on useRef, a hook that offers an efficient way to manage values and DOM elements in React without triggering re-renders. useRef solves the problem of excessive renders by allowing you to store values that persist across renders without affecting the component lifecycle. By understanding useRef and its applications, you can optimize your React components, improve performance, and build more responsive applications.

What is useRef?

useRef is a React hook that allows you to store mutable values that persist across renders without triggering a re-render of the component. This makes useRef particularly useful for situations where you need to manage data or manipulate the DOM, but don’t want React to re-render the component every time the value changes.

The two primary use cases for useRef are:

  1. Direct DOM Manipulation: useRef provides a reference to a DOM element, allowing you to interact with it directly, bypassing React’s render cycle.
  2. Persisting Values Across Renders: useRef allows you to track values that should not trigger re-renders, like timers, counters, or scroll positions.

Key Characteristics of useRef:

  • Does not trigger a render: Unlike useState, changes to the useRef value do not cause the component to re-render.
  • Direct DOM manipulation: You can directly interact with a DOM element without involving React’s rendering mechanism.
  • Persistent across renders: Values stored in useRef survive through component re-renders, maintaining their state without causing React to update the UI.

How useRef Works

1. Storing Values without Re-Renders

With useState, any update to the state triggers a re-render of the component. This is useful when the UI needs to reflect changes, but it can cause excessive renders when managing values that don’t affect the UI.

useRef allows you to store values (like counters or previous states) that persist across renders without affecting the component’s lifecycle. Here’s a simple example:

tsx
import React, { useRef } from "react";

function CounterWithRef() {
  const countRef = useRef(0); // Track count without re-rendering

  const handleClick = () => {
    countRef.current += 1;
    console.log("Count value:", countRef.current); // Check console for updates
  };

  return <button onClick={handleClick}>Click me</button>;
}

In this example, useRef is used to store the count. Clicking the button updates the count without triggering a re-render. The console logs the new count, but the UI remains unchanged. This approach is especially useful when tracking values like scroll position, timers, or form input states that don’t need to be reflected in the UI.


2. Direct DOM Manipulation

One of the most common use cases for useRef is directly interacting with DOM elements. React discourages imperative manipulation of the DOM but offers useRef as a way to safely access and modify DOM elements without triggering a re-render.

For example, focusing an input field imperatively:

tsx
import React, { useRef } from "react";

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

  const handleFocus = () => {
    if (inputRef.current) {
      inputRef.current.focus(); // Directly focus the input element
    }
  };

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

Here, useRef is used to reference the input element in the DOM. When the button is clicked, the input field is focused without causing the component to re-render. The focus ring appears because the browser directly handles the DOM update, but React’s virtual DOM remains unaffected.


How useRef Differs from useState

The primary difference between useRef and useState lies in how they handle updates:

  • useState triggers re-renders: Any update to the state using useState causes React to re-render the component, which is necessary when the UI needs to reflect the updated state.
  • useRef does not trigger re-renders: Changes to the .current property of useRef do not cause the component to re-render. This is ideal for tracking values that don’t need to be reflected in the UI, like timers, previous states, or DOM element references.

Why useRef Can Improve Performance

Using useRef strategically can improve your app’s performance by avoiding unnecessary renders, especially in scenarios where the value being tracked doesn’t impact the UI.

1. Tracking Non-Visual Values

For values like timers or scroll positions, using useState would cause frequent re-renders, potentially degrading performance. useRef allows you to store and update these values without React re-rendering the component.

Example: Tracking Scroll Position

tsx
import React, { useRef, useEffect } from "react";

function ScrollTracker() {
  const scrollPosRef = useRef(0); // Track scroll position without re-renders

  useEffect(() => {
    const handleScroll = () => {
      scrollPosRef.current = window.scrollY;
      console.log("Scroll Position:", scrollPosRef.current);
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return <p>Scroll Position: (check console)</p>;
}

In this example, the scroll position is tracked using useRef, and the value is logged to the console. The component doesn’t re-render on each scroll, which is especially useful when dealing with large pages or frequently updated data.


2. Preventing Performance Bottlenecks in Forms

In complex forms where every keystroke can trigger validation or state changes, using useState could lead to excessive re-renders. With useRef, you can track the input value without causing unnecessary renders, ensuring the form remains responsive.

Example: Efficient Input Handling

tsx
import React, { useRef, useState } from "react";

function FormWithRef() {
  const valueRef = useRef("");
  const [error, setError] = useState("");

  const validate = (val: string) => {
    if (val.length < 5) {
      setError("Too short");
    } else {
      setError("");
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    valueRef.current = e.target.value;
    validate(valueRef.current); // Validate without triggering a re-render
  };

  return (
    <div>
      <input type="text" onChange={handleChange} />
      <p>{error}</p>
    </div>
  );
}

In this example, the input value is tracked using useRef, and the component only re-renders when the validation error needs to be updated.


Conclusion

In React, useRef provides a powerful way to manage persistent values and directly manipulate the DOM without triggering unnecessary re-renders. By understanding when and how to use useRef, you can optimize performance in your applications, especially in scenarios involving DOM manipulation, timers, form handling, and non-visual data.

While useState is ideal for managing state that affects the UI, useRef excels at tracking values that don’t need to be reflected in the UI, providing a more efficient approach in many cases. By leveraging useRef, you can reduce render cycles, avoid performance bottlenecks, and build more responsive applications.