Mastering React's useRef Hook: Why it Doesn't Trigger Re-renders and How it Persists Across Re-renders
Published: 2023-08-24 14:47:06

### 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`**:

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

  return refObject;
}

```
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:

```javascript
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>
  );
}

```
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.

```javascript
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>
  );
}
```

### 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.

```javascript
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>
  );
}

```

### 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.

```javascript
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>
  );
}

```

### 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.