Back

React Development Made Easy: Working with the useRef Hook

React Development Made Easy: Working with the useRef Hook

What is the useRef React hook? This article will give you information about it: You’ll learn what useRef is, how to use it, why it’s crucial for web development using React, and where to use it. You will also see the forwardRef and useImperativeHandle hooks in action.

useRef is a built-in function that allows you to create a reference to a DOM(Document Object Model) element, enabling direct interaction with it.

Why is useRef important for DOM manipulation and element access?

  • Efficiency: useRef minimizes unnecessary re-renders, improving performance when working with DOM elements.
  • Preserving State: It allows you to persist and maintain element states across renders.
  • Child Component Communication: Enables parent components to share element references with child components.
  • React Ecosystem Compatibility: Integrates smoothly with React’s declarative approach and third-party libraries.
  • Cleaner Code: Promotes clean and readable code by encapsulating DOM interactions within React components.
  • Improved Debugging: Aligns with React’s component lifecycle, simplifying debugging and issue resolution.

useRef is a valuable hook in React development because it allows you to directly access and interact with DOM elements, and a significant advantage of using useRef over traditional DOM manipulation lies in its ability to optimize performance. useRef ensures that modifications to the DOM do not result in complete re-renders of the component, thus enhancing overall performance.

In what real-world situations can useRef be effectively used?

useRef can be used in various scenarios, and I’ll highlight a few:

  • Animating Elements
  • Creating Timer
  • Interacting with Audio and Video Element
  • Focus Management
  • Integration with Third-Party Libraries

Getting Started with useRef

To begin, Let’s build a basic feature for focus management. No need to delay – let’s dive right in.

Like with any other built-in hook, the first step is to import the hook from React, as demonstrated below.

import { useRef } from "react";

Next, we invoke the hook within our functional component. It’s important to note that React hooks are only valid within functional components. To explore the rules and guidelines governing the use of React hooks, you can follow this link.

function ParentComponent() {
 let initialValue = null;
 const inputRef = useRef(initialValue);
}

Let’s walk through the code snippet above. In this functional component, I declare an initialValue variable and assign it null because, at this point, it doesn’t reference any DOM elements. Keep in mind that the choice of the initial value may vary based on your specific use case.

Next, I called the hook and provided the initial value as an argument. When you invoke the useRef hook, it returns a reference, an object featuring a single property named current. It’s important to note that this reference object is initially set to the provided initial value.

Before we continue, let’s optimize the code into a single line and console.log the reference object to confirm the output.

function ParentComponent() {
 const inputRef = useRef(null);
 console.log(inputRef); // {current:null}
}

In the revised code snippet below, I linked the reference to the input field using the ref attribute provided by React. The ref attribute is built into React and does not require additional imports.

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

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

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

Did I call .focus on null, considering that null is the initial value assigned to the current property? No, I didn’t. Once the component is rendered, React will populate the current property of the inputRef with the DOM element it is connected to, which, in this case, is the input field. Clear? If not, let’s examine the output together.

Keep in mind that this action occurs after the component has finished mounting. Therefore, we will employ the useEffect hook to inspect the value. Why choose the useEffect hook? It’s because the useEffect hook runs after the component has successfully mounted.

import { useRef, useEffect } from "react";

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

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

 useEffect(() => {
   import { useRef } from "react";
   console.log(inputRef.current); //<input type="text" placeholder="Click the button to focus me"/>
 }, []);

 console.log("I am rendered only once!");

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

I hope you haven’t forgotten to import the useEffect hook. If you have, remember that it will trigger an error, and you need to import built-in hooks from react to resolve the error.

Let’s proceed with our example. When you click the button, the input field will be focused. It’s important to note that this happens without triggering a complete component re-render, making it a performance-efficient approach that outperforms traditional DOM manipulation. Let’s test the code now.

Before clicking on the button: unfoccused input field

After clicking on the button: focussed input field

Open your developer console, and you’ll notice that we have only one output, regardless of how many times you click the button. This demonstrates that the component doesn’t undergo re-renders. Console output

Implementing useRef and Sharing with Child Components

Now that we’ve grasped the concept of useRef and its usage in the parent component, let’s explore how we can pass it to a child component.

Passing a ref to a child component serves several purposes, including:

  • Accessing DOM Elements: It allows you to directly access and manipulate the child component’s DOM elements.
  • Facilitating Parent-Child Communication: It enables seamless communication between parent and child components.
  • Integrating with Third-party Libraries: It aids in integrating with third-party libraries that need direct access to the DOM.
  • Implementing Custom Hooks: ref can implement custom hooks, encapsulating complex logic.
  • Form Validation and Control: It’s useful for managing form validation and control within child components.

Let’s consider some examples that illustrate some of the mentioned purposes.

Allow me to provide an overview of what we’re about to create. In this example, the child components will render a video controlled by the parent component. This will involve discussing another hook, useImperativeHandle, and utilizing the forwardRef wrapper.

Remember that the child component doesn’t expose its DOM node to its parent component; this is where forwardRef comes in. Also, instead of allowing the parent component to access the entirety of the child component node, we will make use of the useImperativeHandle to expose custom value.

Let’s see all this in action. We will start by creating the parent component.

import React, { useRef } from "react";
import exampleVideo from "./asset/example.mp4";

function ParentComponent() {
 const videoRef = useRef(null);

 const handlePlay = () => {
   videoRef.current.play();
 };

 const handlePause = () => {
   videoRef.current.pause();
 };

 return (
   <div>
     <ChildComponent ref={videoRef} source={exampleVideo} />
     <button onClick={handlePlay}>Play Video</button>
     <button onClick={handlePause}>Pause Video</button>
   </div>
 );
}

Let’s walk through the provided code snippet. As previously mentioned, we’re importing useRef from React, and we’re also importing a video file for this example.

In the ParentComponent, we create a reference (videoRef) using the useRef hook and initialize it with a value of null.

I also define two functions:

  • handlePlay function, which, when invoked, plays the video element referred to by videoRef using the play() method.
  • handlePause function, which, when invoked, pauses the video element referred to by videoRef using the pause() method.

Within the return block, we have a ChildComponent that receives the videoRef as a ref and the exampleVideo as a source. This implies that the ChildComponent is expected to be a component that can handle video rendering and playback using the ref and source props.

We also have two buttons: “Play Video” and “Pause Video”. When clicked, they trigger the handlePlay and handlePause functions, allowing you to control the video playback within the ChildComponent.

Let’s go ahead and create the ChildComponent.

import { forwardRef } from "react";

const ChildComponent = forwardRef((props, ref) => {
 return (
   <video ref={ref} controls>
     <source src={props.source} type="video/mp4" />
   </video>
 );
});

In this snippet above, we define a ChildComponent using the forwardRef function from React. forwardRef is a way to pass a ref from a parent component to a child component. In this case, ChildComponent is expected to receive a ref from its parent, which will be used to reference the <video> element it renders.

We imported forwardRef from React.

We defined the ChildComponent using the forwardRef function. This function takes a callback that receives two arguments: props and ref.

Inside the component, we returned a <video> element. The ref attribute is set to the ref passed from the parent component. This allows the parent component to access the <video> element and control its playback.

The <video> element has the controls attribute, which provides standard video player controls like play, pause, and volume.

Inside the <video> element, we have a <source> element. It specifies the video source using the props.source value, which is passed from the parent component. In this case, it’s expected to be an MP4 video file.

Here is the output:

Are you curious about where the useImperativeHandle hook fits into the picture? Let’s explore it together. Stick around for the answer.

The previous way we implemented the feature works fine, as seen in the output above, but it’s only necessary when the ParentComponent requires full access to the ChildComponent’s DOM node. In this scenario, we need access to the pause and play methods of the video node, and that’s where useImperativeHandle becomes relevant.

The useImperativeHandle hook enables us to provide a handle with only the methods the parent component should be able to call.

Let’s continue with the implementation.

const ChildComponent = forwardRef((props, ref) => {
 const childVideoRef = useRef();

 useImperativeHandle(ref, () => ({
   play: () => {
     childVideoRef.current.play();
   },
   pause: () => {
     childVideoRef.current.pause();
   },
 }));

 return (
   <video ref={childVideoRef} controls>
     <source src={props.source} type="video/mp4" />
   </video>
 );
});

In this snippet above, the ChildComponent has been updated to utilize the useImperativeHandle hook to expose specific methods to the parent component, allowing control over the video playback.

We still use the forwardRef function to enable the parent component to pass a ref to this child component.

Inside the ChildComponent, a new childVideoRef is created using the useRef hook. This reference is used to store a reference to the <video> element rendered within the child component.

The useImperativeHandle hook is used to define what gets exposed through the ref passed by the parent component. useImperativeHandle takes two arguments: the ref and a callback function that defines what methods or properties should be exposed to the parent component.

In the callback function, two methods are defined: play and pause. These methods use the childVideoRef to call the play() and pause() methods on the <video> element, respectively. This effectively allows the parent component to trigger these actions on the video element within the child component.

Remember that the methods you expose through useImperativeHandle don’t have to perfectly match the corresponding DOM methods.

Inside the return block, the <video> element uses the childVideoRef as its ref. This means that the ref passed by the parent component is associated with this specific <video> element.

The <video> element also includes the controls attribute, which provides standard video player controls, and a <source> element to specify the video source, just like in the previous version of the code.

Below is the complete code for this example:

import ReactDOM from "react-dom/client";
import "./index.css";

import React, { useRef, forwardRef, useImperativeHandle } from "react";
import exampleVideo from "./asset/example.mp4";

const ChildComponent = forwardRef((props, ref) => {
 const videoRef = useRef();

 useImperativeHandle(ref, () => ({
   play: () => {
     videoRef.current.play();
   },
   pause: () => {
     videoRef.current.pause();
   },
 }));

 return (
   <video ref={videoRef} controls>
     <source src={props.source} type="video/mp4" />
   </video>
 );
});

const ParentComponent = () => {
 const videoRef = useRef();

 const handlePlay = () => {
   videoRef.current.play();
 };

 const handlePause = () => {
   videoRef.current.pause();
 };

 return (
   <div>
     <ChildComponent ref={videoRef} source={exampleVideo} />
     <button onClick={handlePlay}>Play Video</button>
     <button onClick={handlePause}>Pause Video</button>
   </div>
 );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

Here is the output:

Lastly, Let us see an example where we expose a custom method using the useImperativeHandle.

import ReactDOM from "react-dom/client";
import "./index.css";
import React, { useRef, forwardRef, useImperativeHandle } from "react";

const ChildComponent = forwardRef((props, ref) => {
 const inputRef = useRef();

 useImperativeHandle(ref, () => ({
   focus: () => {
     inputRef.current.focus();
   },
   validate: () => {
     return inputRef.current.value.trim() !== ""; // Custom validation logic
   },
 }));

 return <input type="text" ref={inputRef} {...props} />;
});

const ParentComponent = () => {
 const parentInputRef = useRef();

 const handleFocusTextInput = () => {
   parentInputRef.current.focus();
 };

 const handleValidate = () => {
   const isTextInputValid = parentInputRef.current.validate();
   console.log(isTextInputValid);
 };

 return (
   <div className="app">
     <ChildComponent ref={parentInputRef} placeholder="Enter text" />
     <button onClick={handleFocusTextInput}>Focus Input</button>
     <button onClick={handleValidate}>Validate Input</button>
   </div>
 );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

Let me walk you through the snippet above.

  • In the ChildComponent, we initialized a new reference (inputRef) and attached it to the rendered <input> element. This initialization is done in the child component to prevent the parent component from directly accessing the entire input element.

  • The useImperativeHandle defines and exposes two methods, focus and validate, through the ref. These methods allow the parent component to interact with the child input element. The focus function sets focus on the input field, and the validate function performs a custom validation logic, checking if the input value is not an empty string.

  • In ParentComponent, another ref called inputRef is created.

  • The handleFocusTextInput`` function is called when the "Focus Text Input" button is clicked. This function uses inputRef.current.focus()` to trigger the focus function defined in the ChildComponent, which focuses on the input field.

  • The handleValidate function is called when the “Validate Text Input” button is clicked. It uses inputRef.current.validate() to trigger the validate function defined in the ChildComponent. The result of the validation is logged into the console.

Below is the output:

Before clicking on the focus input button: Unfocused input field

After clicking on the focus input button: Focused input field

After clicking on the Validate input button: With no text: Output of Invalid

With text: Output of valid

And that is a wrap.

Conclusion

In this article, we’ve explored the usage of the useRef hook in React, its ability to manage DOM elements, and how to pass and control references between parent and child components using forwardRef and useImperativeHandle.

You can develop an example of using useRef with third-party libraries, following a similar approach to what we’ve demonstrated. Be sure to refer to React’s documentation for in-depth insights. Also, practicing on your own is crucial for effective learning and mastering this concept.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay