Mastering Refs in React: Practical Guide with useRef and forwardRef

Mastering Refs in React: Practical Guide with useRef and forwardRef

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.

image

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.

image

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

image

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 ✌️