Mastering Refs in React: Practical Guide with useRef and forwardRef
- Hashan Hemachandra
- 18 Mar, 2024
Hi everyone π, hope you are doing well.
Welcome to an in-depth tutorial where we explore the practical use of Reactβs useRef
and forwardRef
. By the end of this post, youβll understand how to effectively use these hooks to control and manipulate DOM elements and component instances in a React application.
Introduction
In React, managing focus, reading or manipulating the DOM directly is generally not recommended as it goes against the core principles of Reactβs declarative nature. However, there are scenarios where direct DOM manipulation becomes necessary, such as managing focus, measuring the size of an element, or integrating with third-party DOM libraries. In such cases, React provides us with useRef
and forwardRef
.
Letβs dive into a practical example to understand how to use useRef
and forwardRef
in a Next.js project that displays emojis and their meanings. Weβll be building a feature where clicking an emoji scrolls the corresponding meaning into view.
Project Structure
Our Next.js project is structured as follows:
use-ref-hook/
βββ .next/
βββ app/
β βββ favicon.ico
β βββ globals.css
β βββ layout.tsx
β βββ page.tsx
βββ components/
β βββ EmojiRow.tsx
β βββ EmojiTitle.tsx
β βββ MeaningCard.tsx
β βββ MeaningList.tsx
βββ constants/
β βββ emojiData.tsx
β βββ meaningData.tsx
βββ node_modules/
βββ public/
βββ types/
β βββ types.ts
βββ .eslintrc.json
βββ .gitignore
βββ next-env.d.ts
βββ next.config.mjs
βββ package-lock.json
βββ package.json
βββ postcss.config.js
βββ README.md
βββ tailwind.config.ts
βββ tsconfig.json
-
app: Contains our main page page.tsx.
-
components: Houses our React components including EmojiRow, EmojiTile, MeaningCard, and MeaningList.
-
constants: Stores our data files emojiData.tsx and meaningData.tsx.
-
types: Contains type definitions used in our project.
First Letβs look at our constants.
emojiData.tsx
import { EmojiDataType } from "@/types/types";
export const EMOJI_DATA: EmojiDataType[] = [
{
icon: "π",
name: "Prosper",
},
{
icon: "π€",
name: "Luck",
},
{
icon: "π",
name: "Perfect",
},
{
icon: "π€",
name: "Love",
},
{
icon: "π",
name: "Pray",
},
];
meaningData.tsx
import { MeaningDataType } from "@/types/types";
export const MEANING_DATA: MeaningDataType[] = [
{
name: "Prosper",
description: "Live Long and Prosper",
},
{
name: "Luck",
description: "Fingers crossed, I wish you Good Luck!",
},
{
name: "Perfect",
description: "This is Perfect!",
},
{
name: "Love",
description: "I Love You π",
},
{
name: "Pray",
description: "God will always be with you",
},
];
My plan is to when I click on Emoji Tile named Propser I am going to reference the Meaning Card inside the Meaning List.
Using βuseRefβ in βpage.tsxβ
In our page.tsx
, we utilize useRef
to create references for different emoji meanings:
import { useRef } from "react";
const Home = () => {
const meaningRefs = {
Prosper: useRef(null),
Luck: useRef(null),
Perfect: useRef(null),
Love: useRef(null),
Pray: useRef(null),
};
};
Full code
"use client";
import EmojiRow from "@/components/EmojiRow";
import MeaningList from "@/components/MeaningList";
import { useRef } from "react";
const Home = () => {
const meaningRefs = {
Prosper: useRef(null),
Luck: useRef(null),
Perfect: useRef(null),
Love: useRef(null),
Pray: useRef(null),
};
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-10">
<h1 className="text-3xl font-medium">Mastering Refs in React π§</h1>
<div className="flex gap-7">
<EmojiRow meaningRefs={meaningRefs} />
</div>
<div className="flex flex-col gap-7 w-[500px] max-h-[calc(100vh-910px)] overflow-y-auto shadow-inner">
<MeaningList meaningRefs={meaningRefs} />
</div>
</div>
);
};
export default Home;
Here, useRef
is used to create an object meaningRefs
where each property is a ref corresponding to a particular meaning. useRef
returns a mutable ref object whose .current
property is initialized to the passed argument (null in this case). The object persists for the full lifetime of the component.
We create a dictionary of meaning references using the useRef hook. Then insert the whole object into props. To choose which key-value pair to use from the meaningRefs object passed as a prop, we would typically access the pair by its key. The key is a string that identifies the particular meaning ref we want to use.
The βEmojiTileβ Component
The EmojiTile
component represents an individual emoji that users can interact with. When clicked, it triggers a scroll action that brings the corresponding meaning into view. Hereβs how we implement the EmojiTile
:
"use client";
import { EmojiDataType } from "@/types/types";
interface EmojiTileProps {
emoji: EmojiDataType;
onClick: () => void;
}
const EmojiTile = ({ emoji, onClick }: EmojiTileProps) => {
return (
<div
className="w-20 h-20 shadow flex items-center justify-center rounded-md bg-black/5 hover:bg-black/10 cursor-pointer"
onClick={onClick}
>
{emoji.icon}
</div>
);
};
export default EmojiTile;
Implementing Scroll Behavior in βEmojiRowβ
In our EmojiRow
component, we handle the click event on each emoji. When an emoji is clicked, we want to scroll the corresponding meaning into view:
"use client";
import { EMOJI_DATA } from "@/constants/emojiData";
import EmojiTile from "./EmojiTile";
type MeaningRefs = {
[key: string]: React.RefObject<HTMLDivElement>;
};
const EmojiRow = ({ meaningRefs }: { meaningRefs: MeaningRefs }) => {
return EMOJI_DATA.map((emoji, index) => (
<div key={emoji.name} className="">
<EmojiTile
emoji={emoji}
onClick={() =>
meaningRefs[emoji.name].current?.scrollIntoView({
behavior: "smooth",
})
}
/>
</div>
));
};
export default EmojiRow;
The βMeaningListβ Component
The MeaningList
component is responsible for displaying the list of meanings. Each meaning is wrapped in a div
that is linked to a specific ref, enabling the scroll-into-view functionality when an emoji is clicked:
"use client";
import { MEANING_DATA } from "@/constants/meaningData";
import MeaningCard from "./MeaningCard";
type MeaningRefs = {
[key: string]: React.RefObject<HTMLDivElement>;
};
const MeaningList = ({ meaningRefs }: { meaningRefs: MeaningRefs }) => {
return MEANING_DATA.map((meaning) => (
<div key={meaning.name} ref={meaningRefs[meaning.name]} >
<MeaningCard meaning={meaning} />
</div>
));
};
export default MeaningList;
This component maps over the MEANING_DATA
array, creating a list where each item is a MeaningCard
. By assigning the ref
from meaningRefs
to the wrapping div
, we establish a connection between the emojis and their meanings. When an emoji is clicked, the application scrolls to the corresponding div
in this list.
Forwarding Refs with βforwardRefβ in βMeaningCardβ
In some cases, you might need a child component to expose its DOM node to a parent component. Hereβs where forwardRef
comes into play. In our MeaningCard
component, we wrap the component with forwardRef to forward
a ref from its parent:
"use client";
import { MeaningDataType } from "@/types/types";
import { forwardRef, LegacyRef } from "react";
interface MeaningCardProps {
meaning: MeaningDataType;
}
const MeaningCard = forwardRef(
({ meaning }: MeaningCardProps, ref: LegacyRef<HTMLDivElement> | null) => {
return (
<div
className="h-20 bg-black/5 flex items-center justify-center rounded-md shadow"
ref={ref}
>
{meaning.description}
</div>
);
}
);
MeaningCard.displayName = "MeaningCard";
export default MeaningCard;
By using forwardRef
, MeaningCard
can now receive a ref
that it attaches to its root element. This allows the parent component to directly reference the DOM node.
Expected Behaviour
I hope now you got an idea about how to use useRef
to create references to DOM nodes and how to use forwardRef
to allow child components to expose their DOM nodes to parent components. This pattern is particularly useful in scenarios where you need to directly interact with the DOM, such as controlling focus or scrolling elements into view.
Letβs meet with another practical example in React. Until then, have a good one! Peace βοΈ