The useRef
Hook is a versatile tool, often overlooked but crucial for managing references, optimizing performance, and interacting directly with the Document Object Model (DOM). While it might not be as widely discussed as useState or useEffect, it plays a pivotal role in many React applications.
This guide will discuss the useRef
Hook, its use cases, practical applications, and drawbacks. By the end, you'll be well equipped to leverage its full potential, unlocking new levels of efficiency and control in your React projects.
#What is useRef()?
The useRef()
Hook is a built-in React feature that persists values between component re-renders. Unlike state variables managed by useState
, values stored in a ref object remain unchanged across renders, making it ideal for scenarios where data doesn't directly affect the UI but is essential for the component's behavior.
#How does React useRef() work?
When React encounters a useRef()
Hook, it returns a plain JavaScript object with a single property: current
. This current
property stores mutable values, which can be of any type, from simple values like numbers and strings to complex objects, functions, or even references to DOM elements.
React assigns the initial value you define to the current
property of the returned reference. React will set the value of the useRef to undefined
if you don't provide an initial value. Importantly, you can update this current
value directly without triggering a re-render of the component. This can be seen in the snippet below:
import { useRef } from "react";function MyComponent() {const reference = useRef(true);console.log(reference.current); // true}
Also, the returned reference object is mutable. You can update the current
value directly, as shown in the snippet below.
import { useRef } from "react";function MyComponent() {const reference = useRef(true);const handleUpdate = () => {reference.current = !reference.current;};console.log(reference.current); // truereturn <button onClick={handleUpdate}>Update</button>;}
Clicking the “Update” button changes the reference.current
value from true
to false
and the other way around.
#Use cases of useRef() React Hook
Beyond its ability to persist values, the useRef()
Hook has several vital roles in React applications:
Accessing DOM elements
Imagine a login page where users need to enter their username and password. To enhance the user experience, you can automatically direct their focus to the username field as soon as the page loads.
To achieve this, use the useRef
's capacity to access rendered DOM elements. This feature returns the referenced DOM element and its properties, providing room for direct manipulations.
import { useRef, useEffect } from “react”function Login() {const usernameRef = useRef(null)useEffect(() => {usernameRef.current.focus()}, [])return (<><form><input type="text" ref={usernameRef} /></form></>)}
The above snippet shows a Login
component that renders a form with an input field for the user’s username. Also, it defines a username reference with the useRef
Hook, which the focus()
method is called on. This is executed inside a useEffect
Hook, so it runs immediately after the UI finishes loading.
Tracking previous values
By storing the previous value of the input element as a state variable in a ref, you can monitor changes and address them accordingly. This technique is useful for implementing "undo" functionality, creating custom Hooks that need to compare previous and current values, or optimizing performance by preventing unnecessary calculations.
import { useState, useRef, useEffect } from 'react';function Counter() {const [count, setCount] = useState(0);const previousCountRef = useRef(count); // Store previous count in a refuseEffect(() => {if (previousCountRef.current !== count) {console.log('Count changed:', count, '(Previous:', previousCountRef.current, ')');}previousCountRef.current = count; // Update the ref}, [count]); // Run the effect only when count changesreturn (<div><p>Count: {count}</p><button onClick={() => setCount(count + 1)}>Increment</button></div>);}
In this example, the useRef
Hook stores the previous value of the count
state variable. Every time the count
changes, the useEffect
Hook compares the new count
to the value stored in the previousCountRef
and logs a message to the console if they differ. This way, you can track how the count
value evolves over time.
Managing timers and intervals
The useRef
simplifies the management of timers and intervals within your components. You can store the timer's ID in a ref, allowing you to start, stop, or reset it as needed without triggering unnecessary re-renders.
import { useState, useRef, useEffect } from 'react';function myTimer() {const [seconds, setSeconds] = useState(0);const timerRef = useRef(null); // Ref to store the timer IDconst startTimer = () => {timerRef.current = setInterval(() => {setSeconds((prevSeconds) => prevSeconds + 1);}, 1000); // Update every second};const stopTimer = () => {clearInterval(timerRef.current);};const resetTimer = () => {clearInterval(timerRef.current);setSeconds(0);};// Cleanup function to clear the interval when the component unmountsuseEffect(() => {return () => clearInterval(timerRef.current);}, []);return (<div><p>Time elapsed: {seconds} seconds</p><button onClick={startTimer}>Start</button><button onClick={stopTimer}>Stop</button><button onClick={resetTimer}>Reset</button></div>);}
In this example, a timer component was created using the useState
and useRef
Hooks. The useState
Hook manages the seconds variable, which keeps track of the elapsed time. Meanwhile, useRef
stores a reference to the interval ID (timerRef), which is responsible for updating the seconds every 1000 milliseconds (1 second).
The functions startTimer
, stopTimer
, and resetTimer
control the timer's behavior. startTimer
starts the interval, stopTimer
stops it, and resetTimer
resets the elapsed time to zero.
Building custom Hooks
When creating custom Hooks that need to maintain an internal state between renders, useRef
becomes an invaluable asset. It enables you to store and update data privately within the Hook, promoting reusability and encapsulation of complex logic.
Imagine having multiple components that are required to maintain their previous state. It will appear too tedious to write this logic individually for every component. To avoid this, create a custom Hook using the useRef
Hook as follows:
First, create a Hooks folder and a file usePreviousState.js
, then add the following snippet:
import { useRef, useEffect } from "react";export default function usePreviousState(value) {const ref = useRef();useEffect(() => {ref.current = value;});return ref.current;}
In the snippet above, a useRef
instance was created, then on every page re-render, assign the value passed to the usePreviousState
Hook to the ref.current
. Finally, return the value of the ref.current
.
To illustrate the custom Hook's utility, let’s revisit the 'tracking previous values' scenario
import { useState, usePreviousState, useEffect } from 'react';function Counter() {const [count, setCount] = useState(0);const previousCount = usePreviousState(count); // Store previous count in a custom HookuseEffect(() => {if (previousCount !== count) {console.log('Count changed:', count, '(Previous:', previousCount, ')');}previousCount = count; // Update the previous state}, [count]); // Run the effect only when count changesreturn (<div><p>Count: {count}</p><button onClick={() => setCount(count + 1)}>Increment</button></div>);}
In the snippet above, all instances of the initial useRef
were replaced with the newly created custom Hook. This shows that creating a custom Hook makes it possible to reuse complex logic.
#Optimizing performance
Beyond its DOM manipulation capabilities, useRef shines as a performance enhancer. Imagine you have a computationally intensive task within your component, such as filtering a dataset or performing calculations, re-executing these operations on every render would be wasteful.
Storing values of these calculations within a ref variable using useRef can avoid unnecessary re-computations. Since refs persist across renders without triggering updates, the calculated value remains accessible and doesn't need to be recalculated unless the underlying data changes. Let's look at the example shown below:
import { useState, useRef, useMemo } from "react";export default function calculationComponent() {const [inputNumber, setInputNumber] = useState(10); // Initial Calculation numberconst calculateResult = useRef(null); // Ref to store the calculated resultconst calculateFib = (n) => {if (n <= 1) return n;return calculateFib(n - 1) + calculateFib(n - 2);};// Memoize the calculation using useMemoconst memoizedCal = useMemo(() => {if (calculateResult.current &&calculateResult.current.input === inputNumber) {// If the input hasn't changed, reuse the cached resultreturn calculateResult.current.result;} else {// Calculate the result and store it in the refconst result = calculateFib(inputNumber);calculateResult.current = { input: inputNumber, result };return result;}}, [inputNumber]);return (<div><inputtype="number"value={inputNumber}onChange={(e) => setInputNumber(parseInt(e.target.value, 10))}/><p>Result of {inputNumber} is: {memoizedCal}</p></div>);}
In the snippet above, the useRef
Hook, named calculateResult
, is employed to optimize performance by caching the results of a computationally expensive calculation like the Fibonacci sequence. The calculateResult.current
property stores both the input number and the calculated result, allowing for comparison on subsequent renders.
The useMemo Hook is used to memoize the calculation. It checks if the input number (inputNumber
) has changed since the last render. If not, it returns the cached result from calculateResult.current
, avoiding redundant computation. If the input number changes, the Fibonacci calculation is performed, and the result is stored in calculateResult.current
for future reuse.
This memoization strategy improves the component's performance by preventing recalculations whenever the component re-renders due to unrelated state changes. While useRef
offers performance benefits, knowing its potential drawbacks is essential.
#Drawbacks of useRef Hook
- Manual management: Unlike state variables managed with
useState
oruseReducer
, changes to the.current
property of a ref attribute do not automatically trigger re-renders of your component. This means you need to manage updates and trigger re-renders manually when necessary and can add extra complexity to your code.
- Potential memory leaks: If you're not careful, you can create dangling refs. This happens when a ref is still holding onto a reference to a DOM element or other object that has already been unmounted or removed from the DOM, leading to memory leaks.
- Overuse: While refs are helpful in certain scenarios, overusing them can lead to a more complex and less maintainable codebase.
- Debugging challenges: Since changes to refs don't trigger re-renders, debugging issues related to refs can sometimes be more complicated than debugging state-related problems.
- Not suitable for array or object updates: Although you can store arrays or objects in a ref, updating individual properties or elements within them won't trigger re-renders. You'll need to manually update the entire array or object and trigger a re-render if you want those changes to be reflected in the UI. This can require more work than using state for mutable value structures.
To mitigate these drawbacks
- Use the
useRef
when necessary and prioritize managing UI-related data with state (useState
oruseReducer
) as it automatically triggers re-renders.
- Always ensure proper cleanup of refs when components are removed from the DOM to prevent memory leaks.
- Leverage debugging tools like React DevTools to track ref values and their impact if your code becomes overly reliant on refs.
- Consider alternative approaches like lifting the state up or using context to streamline state management and component communication.
#useRef vs. useState Hook
While both useRef
and useState store values in React components, they serve different purposes and behave differently. Let's look at these Hooks, exploring their characteristics and how they contribute to state management in a React component.
Feature | useRef | useState |
---|---|---|
Mutable | Yes, the .current property can be changed directly | No, state updates must be done through the setter function (e.g., setCount ) |
Persistence across renders | Yes, the value persists for the lifetime of the component | No, the value is reset on each re-render |
Triggers re-render | No, updating the .current property does not cause a re-render | Yes, updating state using the setter function triggers a re-render of the component |
Common use cases | Accessing DOM elements, storing previous values, managing timers/intervals, storing references | Managing UI state, storing data that directly affects the component's rendering |
#useRef Hook vs createRef function
Before the Hooks era, React used createRef() for refs in their application. Let's break down their key distinctions when it comes to managing refs in React applications:
Feature | useRef Hook | createRef Function |
---|---|---|
Component type | Exclusively used in functional components (introduced in React 16.8) | Designed for class components (pre-Hooks era) |
Initial value | Takes an optional initial value, assigned to ref.current (defaults to undefined ) | No initial value, ref.current is initially null |
Re-renders | Returns the same ref object on each re-render, maintaining its value | Creates a new ref object on every re-render, losing the previous value |
Persistence | Value persists for the entire component lifecycle | Value is lost on re-renders unless explicitly stored in a class component's instance variable |
Flexibility | More flexible in functional components due to its persistence | Less flexible, requires additional state management for persistence in class components |
If you're using functional components with React Hooks, useRef
is the way to go. But if you're working with legacy class components, createRef
is your best option.
However, consider refactoring to functional components and the useRef
Hook for a more modern and maintainable approach. Next, let's discuss the useRef
Hook in data fetched from content management systems.
useRef Hook in dynamic content fetched from CMSs
The useRef
Hook, although not inherently tied to Content Management Systems (CMS), plays a crucial role in enhancing user interactions with dynamically fetched content from platforms like Hygraph, a GraphQL-Native Headless Content Management, and Federation System.
By storing references to specific elements within the fetched data, such as images or content blocks, developers can implement features like lazy loading, smooth scrolling, or interactive components.
Furthermore, useRef
is valuable in optimizing data fetching from CMSs by caching results, especially for complex queries or large datasets. This caching mechanism reduces the load on the CMS and improves the application's performance as it avoids redundant data fetching operations.
#Summing it up
While often overlooked, the useRef
Hook is a powerful asset in a React developer's toolkit. It excels in managing references, optimizing performance, and enabling seamless interactions with DOM elements.
Understanding its capabilities and appropriate use cases allows developers to create more efficient, maintainable, and interactive React applications. Whether you're building complex animations, managing timers, or integrating with headless CMS platforms like Hygraph to optimize content delivery, useRef
proves its value repeatedly.
Ready to explore the power of Hygraph? Sign up for a free-forever developer account and experience the seamless integration of Hygraph with React and its powerful Hook like useRef
.