DEV Community

Saman Abbasi
Saman Abbasi

Posted on

Mastering React's useRef Hook: Why it Doesn't Trigger Re-renders and How it Persists Across Re-renders

Introduction:

React's useRef hook is a powerful tool that often perplexes newcomers due to its unique behavior. Unlike state variables, modifying a useRef object's current property doesn't trigger component re-renders. Additionally, useRef exhibits a remarkable capability—it persists data across re-renders. In this comprehensive guide, we will explore why useRef behaves this way, the technical underpinnings behind it, and practical use cases. Let's embark on a journey to master useRef in React.

Closures: The Cornerstone of useRef

To comprehend why useRef maintains state and avoids re-renders, we must first grasp the concept of closures in JavaScript. Closures allow functions to remember and access variables from their lexical scope even when executed outside that scope. In the context of useRef, closures are the key to maintaining references across re-renders.

Consider a simplified implementation of useRef:

function useRef(initialValue) {
  const refObject = {
    current: initialValue,
  };

  return refObject;
}

Enter fullscreen mode Exit fullscreen mode

In this implementation, the current property resides within refObject, a JavaScript object. Critically, refObject acts as a closure, capturing and preserving the current property across re-renders.

During re-renders, React doesn't create a new closure for refObject. Instead, it leverages the existing closure that references refObject. This behavior ensures that changes made to current persist between renders.

Immutability and React Rendering

To appreciate why useRef doesn't trigger re-renders, we must delve into React's rendering mechanism and its reliance on immutability. React operates on the principle that when it detects changes in a component's state or props, it re-renders that component.

However, the useRef object's current property can be modified without triggering a re-render. Consider this example:

import React, { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(null);

  const handleButtonClick = () => {
    // Modifying the current property doesn't trigger a re-render
    myRef.current.textContent = 'Button Clicked';
  };

  return (
    <div>
      <button onClick={handleButtonClick}>Click Me</button>
      <p ref={myRef}>Initial Text</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, when the button is clicked, the textContent of myRef.current is modified. Remarkably, this change doesn't cause the component to re-render. Why? Because the myRef object itself remains the same.

React relies on the comparison of previous and current values to determine if a re-render is necessary. Since the identity, or reference, of the myRef object doesn't change when we update its current property, React doesn't consider it a state or prop change that would trigger a re-render.

Identity and Reconciliation in React

To fully grasp why useRef remains consistent across re-renders, we need to explore React's reconciliation process. React uses reconciliation to determine whether a component should update. It does so by comparing the new virtual DOM (vDOM) with the previous one.

When React identifies that the identity (in this case, the reference) of a component or element hasn't changed, it considers it the same and skips a re-render. This process is central to React's performance optimization strategy.

Consistency Across Renders

React places a strong emphasis on maintaining consistency for the current property within a useRef object across renders. This consistency is ensured by creating the useRef object only once during the initial render. Subsequent renders reuse the same object. Consequently, any changes made to the current property persist across renders.

This consistency is a powerful feature of useRef because it allows developers to maintain state between renders without causing unnecessary re-renders. Whether you're storing references to DOM elements or memoizing values, useRef provides a reliable and performant mechanism for achieving this.

Practical Applications and Best Practices:

Beyond understanding the inner workings of useRef, it's essential to explore its practical applications and best practices.

1. Accessing and Manipulating DOM Elements:

useRef is frequently used to interact with DOM elements directly. This is particularly useful when you need to perform actions such as focusing on an input field, scrolling to a specific element, or animating elements.

import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  const handleFocusButtonClick = () => {
    // Focus on the input element using useRef
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocusButtonClick}>Focus Input</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Storing Mutable Values Without Re-renders:

Unlike state variables, changes to a useRef object's current property do not trigger re-renders. This makes useRef an excellent choice for storing values that don't impact your component's UI but need to persist between renders.

import React, { useState, useEffect, useRef } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const previousCountRef = useRef(0);

  useEffect(() => {
    // Update the previous count when count changes
    previousCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {previousCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

3. Optimizing Performance:

useRef can also be a valuable tool for optimizing performance. You can use it to memoize expensive calculations, ensuring they are only recomputed when necessary.

import React, { useState, useEffect, useRef } from 'react';

function MyComponent() {
  const [inputValue, setInputValue] = useState('');
  const [result, setResult] = useState(null);
  const computationCache = useRef({});

  useEffect(() => {
    if (!computationCache.current[inputValue]) {
      // Perform the expensive calculation and store the result in the cache
      computationCache.current[inputValue] = performExpensiveCalculation(inputValue);
    }

    // Update the result with the cached value
    setResult(computationCache.current[inputValue]);
  }, [inputValue]);

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <p>Result: {result}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Conclusion:

In conclusion, useRef is a versatile tool in the React developer's toolkit. By harnessing closures, immutability, and React's reconciliation process, useRef maintains state across re-renders without triggering unnecessary updates. Understanding these mechanisms empowers React developers to build efficient, responsive, and performant applications. Whether you're working with DOM elements, memoizing values, or optimizing performance, useRef is a valuable asset in your development journey. Mastering it opens up a world of possibilities for enhancing both your coding skills and the user experience of your React applications.

Top comments (0)