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 theforwardRef
anduseImperativeHandle
hooks in action.
Discover how at OpenReplay.com.
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:
After clicking on the button:
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.
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
andvalidate
, through theref
. These methods allow the parent component to interact with the child input element. Thefocus
function sets focus on the input field, and thevalidate
function performs a custom validation logic, checking if the input value is not an empty string. -
In
ParentComponent
, anotherref
calledinputRef
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 usesinputRef.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:
After clicking on the focus input button:
After clicking on the Validate input button: With no text:
With text:
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.