Master the art of forward refs: take control of your React components

Master the art of forward refs: take control of your React components

you can read my article on useRef hook

As a developer working with React, you’re always looking for ways to make your code more efficient and easier to maintain. That’s where forward refs come in.

With forward refs, you can easily access the DOM elements of your React components, allowing you to manipulate them in ways that weren’t previously possible.
This can be especially useful for controlling focus, text selection, or media playback. Plus, with forward refs, you can pass refs between components, making it easier to build complex and flexible UI systems.

In short, if you’re a React developer, you don’t want to miss out on the power of forward refs. So make sure to learn more about them today!

forward ref

forwardRef lets your component expose a DOM node to the parent component with a ref.

Reference

forwardRef(render)

Call forwardRef() to let your component receive a ref and forward it to a child component:

import { forwardRef } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  // ...
});

Parameters

  • render: The render function for your component. React calls this function with the props and ref that your component received from its parent. The JSX you return will be the output of your component.

Returns

forwardRef returns a React component that you can render in JSX. Unlike React components defined as plain functions, a component returned by forwardRef is also able to receive a ref prop.

let's take an example

Here we are increasing the font size, decreasing the font size, ad changing the color.

let’s deep dive into this

let’s start with the App.jsx

import Heading from "./components/Heading";
import { useRef } from "react";
import "./App.css";

function App() {
  const headingRef = useRef(null);
  const handleClick = () => {
    headingRef.current.changeColor();
  };
  const increaseFontSize = () => {
    headingRef.current.increaseFontSize();
  };
  const decreaseFontSize = () => {
    headingRef.current.decreaseFontSize();
  };
  return (
    <div className="App">
      <Heading ref={headingRef} />
      <button onClick={handleClick}>Click Me To Change The Text Color</button>
      <button onClick={increaseFontSize}>Increase The Font Size</button>
      <button onClick={decreaseFontSize}>Decrease The Font Size</button>
    </div>
  );
}

export default App;

Here we are just passing the headingRef to the heading component, and by using current property we are manipulating the heading component.

Now let's take a look at where we’re getting these changeColor() increaseFontSize() decreaseFontSize() functions from.

now let’s look into Heading.jsx

for now, let’s make it short….

import React, { forwardRef, useImperativeHandle, useRef } from "react";

const Heading = forwardRef(function Heading(_props, ref) {
  const newHeadingRef = useRef(null);

  useImperativeHandle(
    ref,
    () => {
      return {
        increaseFontSize() {
          newHeadingRef.current.style.fontSize = "100px";
        }
      };
    },
    []
  );
  return (
    <h1 style={{ color: "white" }} ref={newHeadingRef}>
      Heading
    </h1>
  );
});

export default Heading;

Here we are using forward ref so that we can get the ref from parent's component is App.jsx

we receive ref as a second parameter after props.

Now you’ll notice one thing we are not passing the ref to the tag h1directly; instead, we are creating the new ref that is newHeadingRef and we are passing it to the h1 tag.

And we are using a new hook that’s useImperativeHandle you might be familiar with or not. so don’t worry; in the next step we are going to cover it why we are using it and what it.

useImperativeHandle

useImperativeHandle is a React Hook lets you customize the handle exposed as a ref.

useImperativeHandle(ref, createHandle, dependencies?)

Reference

useImperativeHandle(ref, createHandle, dependencies?)

Call useImperativeHandle at the top level of your component to customize the ref handle it exposes:

Parameters

  • ref: The ref you received as the second argument from the forwardRef render function.

  • createHandle: A function that takes no arguments and returns the ref handle you want to expose. The ref handle you return have any type. Usually, you will return an object with the methods you want to expose.

  • optional dependencies: The list of all reactive values referenced inside of the createHandle code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison algorithm. If a re-render resulted in a change to some dependency, or if you did not specify the dependencies at all, your createHandle function will re-execute, and the newly created handle will be assigned to the ref.

const newHeadingRef = useRef(null);
useImperativeHandle(ref,() => {
      return {
        increaseFontSize() {
          newHeadingRef.current.style.fontSize = "100px";
        }
      };
    },[]);

Suppose you don’t want to expose the entire h1 DOM node, but you want to expose one of its methods: fontSize To do this, keep the real browser DOM in a separate ref. Then use useImperativeHandle to expose a handle with only the methods that you want the parent component to call:

Now, if the parent component gets a ref to h1, it will be able to call the increaseFontSize methods on it. However, it will not have full access to the underlying <h1> DOM node.

so here is the full code for Heading.jsx

import React, { forwardRef, useImperativeHandle, useRef } from "react";

const Heading = forwardRef(function Heading(_props, ref) {
  const newHeadingRef = useRef(null);

  useImperativeHandle(
    ref,
    () => {
      return {
        changeColor() {
          if (newHeadingRef.current.style.color === "white") {
            newHeadingRef.current.style.color = "red";
            return;
          }
          if (newHeadingRef.current.style.color === "red") {
            newHeadingRef.current.style.color = "white";
            return;
          }
        },
        increaseFontSize() {
          newHeadingRef.current.style.fontSize = "100px";
        },
        decreaseFontSize() {
          newHeadingRef.current.style.fontSize = "50px";
        },
      };
    },
    []
  );
  return (
    <h1 style={{ color: "white" }} ref={newHeadingRef}>
      Heading
    </h1>
  );
});

export default Heading;

Pitfall

Do not overuse refs. You should only use refs for imperative behaviors that you can’t express as props: for example, scrolling to a node, focusing a node, triggering an animation, selecting text, and so on.

If you can express something as a prop, you should not use a ref. For example, instead of exposing an imperative handle like { open, close } from a Modal component, it is better to take isOpen as a prop like <Modal isOpen={isOpen} />. Effects can help you expose imperative behaviors via props.

you can read my article on useRef hook

Hope you like it 🤗

Happy coding!

want to give suggestions:-

find me on LinkedIn Twitter

Email me at