Discover how to use React refs effectively to optimize performance, manage DOM interactions, and avoid unnecessary re-renders. Learn with practical examples like debounced search, form resets, and more in this comprehensive guide.
In my work as a developer, I often found myself confused by the concept of React refs. I realised that I wasn’t alone, many of us struggle to understand how refs work and how to use them effectively. That’s why I decided to dig deeper into the topic and share what I learned with the community. With not much time spent on the basics, we will try to learn how using refs can be beneficial.
Outline
- Short on Refs, What are they?
- Use Cases for Refs
- Optimise Search with ref
- Why Refs can be confusing
- What not to use Refs for?
- Wrapping Up
Short on Refs, What are they?
“A mutable variable that persists data between renders, but modifying it won’t trigger a component re-render.”
thats it? yes.
So, let’s say your website features a blog and you want to track how long a user spends on each post for analytics purposes. Use refs to store this duration. Why? Because you don’t want to display that value in the UI, and updating it won’t trigger any re-renders.
That’s the basics we need to know about!
Use Cases for Refs
Let’s explore real-world scenarios where refs can be useful.
Usage: Preserve Your Previous Values
Resetting Form Values:
Imagine you have a form embedded within a modal on your application. Every time you open the modal, you fill out the form. However, sometimes you might change a few fields and close the modal without saving. In this case, you can use React refs to store the initial field values on the first render. If the user closes the modal without saving the new values, you can simply reset the form to its original state using the data stored in the ref. This ensures the form reverts to its default values, improving user experience and data consistency.
Preview Dark Mode Without Saving:
Consider a scenario where a user toggles to “dark mode” as a temporary preview on your website. Instead of immediately applying the new theme, you can store the current (original) theme in a ref. This allows the user to see how the page would look in dark mode without permanently changing the setting. If the user clicks “Cancel,” you can revert the page to the original theme using the stored ref value. However, if the user decides to apply the dark mode, the new theme is retained. This method provides a seamless and non-disruptive preview experience while maintaining optimal performance.
Usage: Debouncing
When users type into a search box, triggering an API call on every keystroke can cause performance issues. By using a React ref to store a debounce timer, we can delay API calls. Instead of calling the API on every keystroke, it is triggered only after the user stops typing. This approach reduces unnecessary API calls, improves performance, and creates a smoother search experience.
Usage: Selective Exposure
Sometimes, you might only want to share a few specific functions from a child component with the parent. For example, when you store a DOM element in a ref, it comes with many methods like focus, innerHTML, innerText, and scrollIntoView. But what if you only want the parent to access just the focus method? With React’s useImperativeHandle hook, you can customize the ref’s API to expose only the functions you need, keeping the component's internals neatly encapsulated.
Usage: Accessing DOM in HOCs
Suppose you have a reusable button component that’s wrapped in an HOC to add logging or theming. If the parent component needs to call methods like focus() on the underlying button, you can forward the ref through the HOC. This allows the parent to interact directly with the DOM element even though it’s wrapped by extra functionality.
Optimise Search with Refs
Now, let’s explore one of the examples mentioned above in detail.
We have a scenario where we need to implement a feature with a search box on a website that allows users to search for books. As users types into the search box, a GET API call is made to fetch book data that matches their input.

Implementation
// src/components/SearchInput.tsx
import React, { useState } from "react";
interface BookData {
authors: string[];
title: string;
subtitle: string;
status: string;
}
export const BookSearch = () => {
const [books, setBooks] = useState<BookData>({
authors: [],
title: "",
subtitle: "",
status: "Search Books",
});
const searchBooks = async (query: string) => {
if (!query)
return setBooks((prev) => ({ ...prev, status: "Search Books" }));
try {
setBooks({ authors: [], title: "", subtitle: "", status: "Fetching..." });
const { docs } = await (
await fetch(
`http://openlibrary.org/search.json?q=${query.split(" ").join("+")}`
)
).json();
if (!docs.length)
return setBooks((prev) => ({ ...prev, status: "No results found" }));
const { author_name, title, subtitle } = docs[0];
setBooks({
authors: author_name || [],
title: title || "No title available",
subtitle: subtitle || "",
status: "",
});
} catch {
setBooks((prev) => ({ ...prev, status: "Error fetching books" }));
}
};
return (
<>
<input
type="search"
placeholder="Search Books..."
onChange={(e) => searchBooks(e.target.value)}
className="searchInput"
/>
<div className="results">
{books.status ? (
<p>{books.status}</p>
) : (
<>
<p>
<b>Title:</b> {books.title}
</p>
<p>
<b>Subtitle:</b> {books.subtitle || "None"}
</p>
<p>
<b>Authors:</b> {books.authors.join(", ") || "Unknown"}
</p>
</>
)}
</div>
</>
What’s the issue here?
When a user types in the search box, the searchBooks function executes on every keystroke, calling the API with the current input, which results in too many API calls. Once the API returns a response, it updates the state using setBooks, which triggers a re-render. Since the API often returns fuzzy matches, the UI may display results based on the current input, leading to inaccurate or inconsistent search results. Simply put, if you trigger an API call on every onChange event, you’ll end up making too many requests. This not only degrades performance but also leads to UI inconsistencies or incorrect results.
This is where refs come in handy. By using refs, we can optimize API calls and ensure accurate results.

Implementation
// src/components/SearchInput.tsx
import React, { useRef, useState } from "react";
interface BookData {
authors: string[];
title: string;
subtitle: string;
status: string;
}
export const BookSearchRef = () => {
const [books, setBooks] = useState<BookData>({
authors: [],
title: "",
subtitle: "",
status: "Search Books",
});
const debounceRef = useRef<number | null>(null);
const searchBooks = async (query: string) => {
if (!query)
return setBooks((prev) => ({ ...prev, status: "Search Books" }));
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
try {
setBooks({ authors: [], title: "", subtitle: "", status: "Fetching..." });
debounceRef.current = setTimeout(async () => {
const { docs } = await (
await fetch(
`http://openlibrary.org/search.json?q=${query.split(" ").join("+")}`
)
).json();
if (!docs.length)
return setBooks((prev) => ({ ...prev, status: "No results found" }));
const { author_name, title, subtitle } = docs[0];
setBooks({
authors: author_name || [],
title: title || "No title available",
subtitle: subtitle || "",
status: "",
});
}, 700);
} catch {
setBooks((prev) => ({ ...prev, status: "Error fetching books" }));
}
};
return (
<>
<input
type="search"
placeholder="Search Books..."
onChange={(e) => searchBooks(e.target.value)}
className="searchInput"
/>
<div className="results">
{books.status ? (
<p>{books.status}</p>
) : (
<>
<p>
<b>Title:</b> {books.title}
</p>
<p>
<b>Subtitle:</b> {books.subtitle || "None"}
</p>
<p>
<b>Authors:</b> {books.authors.join(", ") || "Unknown"}
</p>
</>
)}
</div>
</>
);
};
Why Refs Can be Confusing?
When I say refs are confusing, it’s because they behave differently when passed as props. Let’s understand why.
With the latest update in React v19 [reference], using refs as props has become straightforward. However, if your project is in React v18 or earlier, it’s important to understand how it works.
Refs as Props Before React v19:
Suppose you need to access the DOM element of a child component. By default, React doesn’t allow a component to directly access the DOM nodes of other components not even its own children! This is intentional, as manually modifying another component’s DOM can make your code more error-prone and difficult to maintain.
That’s why we can’t pass ref as a regular prop. In React, ref is a reserved keyword. If you pass ref as a prop, React treats it as a special property and manages it differently.
So, how do we make this work?
We use forwardRef
. By wrapping a Child component with forwardRef
, we can pass ref
as a regular prop from the parent. It handles ref forwarding in a efficient way. How? let’s see
In React, every update happens in two phases:
- Render Phase — React figures out what should be displayed on the screen.
- Commit Phase — React applies those changes to the DOM.
You generally shouldn’t access refs during rendering. If a ref holds a DOM node, ref.current
will be null during the first render because the DOM hasn’t been created yet. Even during updates, the DOM isn’t updated yet, making it too early to read from refs.
The forwardRef
function updates ref.current
during the commit phase, ensuring the ref points to the correct DOM node after rendering. Before modifying the DOM, it temporarily sets ref.current
to null
, and once the update is complete, it reassigns it to the correct DOM node.
But you may have noticed that renaming the prop (for example, using inputRef instead of the reserved ref) allows refs to be passed. Yes, it works, then what’s the risk? When you rename the prop, you lose React’s automatic handling. That means:
Manual Wiring: Every time you pass the reference down, you must remember to “wire” it manually. If you wrap your component in another layer (like a higher-order component), that wrapper must also know about and pass along your custom prop. Forgetting to do so will “lose” your reference.
Brittle Composition: If the component’s internal structure changes for example, if the DOM node that should receive the ref moves deeper or changes name you must update every place where you manually attach the custom ref.
However, in React v19, refs can now be passed as props natively, eliminating the need for forwardRef in many cases.
What Not to use Refs for?
Managing Component State — Using refs to store state-like data instead of useState.
Triggering Re-Renders — Expecting a UI update when modifying ref.current.
Modifying the DOM Instead of Using React’s Declarative Approach — Manually updating the DOM instead of relying on React’s rendering system.
Refs are powerful, but they should be used wisely. Stick to managing state with useState, avoid expecting re-renders from refs, and let React handle UI updates declaratively.
Wrapping Up
React refs are highly effective when used correctly. They help with DOM interactions, remember values between renders, and improve performance in cases like debounced search and form resets. With React 19, passing refs is simpler, but they should still not be used for state management or direct DOM updates. Knowing when and how to use refs properly keeps your React apps cleaner and more efficient.
I enjoyed sharing my experience, and I hope you found it valuable too! As I’m still exploring this space, I’d love your feedback and suggestions. ☺️ If you see areas where I can improve or have topics you’d like me to cover, please don’t hesitate to reach out! You can also visit my website. I’m excited to learn and grow with your help. 👋🏻
Top comments (0)