Breaking the Rules with useImperativeHandle Hook in React

Breaking the Rules with useImperativeHandle Hook in React

Intro:

In React, a parent component passes props down to a child component, and if the parent component needs to call a function defined in the child component, it typically must be defined in the parent component and then passed down. However, there is a way to break this rule using the useImperativeHandle hook.

The useImperativeHandle:

The useImperativeHandle hook is used to expose functions from a child component to a parent component. The useImperativeHandle hook takes a ref as its first argument, some handles as its second argument, and optional dependencies if any. useImperativeHandle returns undefined. It cannot be used alone and must be used with the useRef hook and the forwardRef component.

The useRef hook is used to create a reference to a DOM node or a value that persists across renders. The forwardRef component is used to pass the ref from the parent component to the child component.

Let's take a look at the basic syntax to understand all those jargon.

Basic Syntax:

useImperativeHandle(ref, createHandle, dependencies?)
// or 
useImperativeHandle(ref, () => {
    return {
      // ... your methods ...
    };
  }, []);

Examples:

Let's start better understanding by looking at the following diagram. The useRef hook was defined in the parent component and passed as props to the child component. In the child component, we access the ref and passed it down to useImperativeHandle. useImperativeHandle lets us customize the handle exposed as a ref. The createHandle is a function that takes no arguments and returns the ref handles we want to expose. That ref handle can have any type. Usually, you will return an object with the methods you want to expose. The forwardRef is used to expose any DOM node to the parent. To do that, wrap your component definition into forwardRef which will receive a ref as the second argument after props.

Parent Component:

import { useRef } from 'react';
import Modal from './Modal';
function App() {
  const modalRef = useRef();

  const handleOpenModal = () => {
    modalRef.current.openModal();
  }
  console.log('parent rendered')
  return (
    <main className="App">
      <p>Parent Component</p>
      <Modal ref={modalRef} />
      <button onClick={handleOpenModal}>Open</button>
    </main>
  );
}
export default App;

Child Component:

import { forwardRef, useImperativeHandle, useState } from "react";
const Modal = (props, ref) => {
    const [modalState, setModalState] = useState(false);
    useImperativeHandle(ref, () => ({
        openModal: () => setModalState(true)
    }));
    console.log('child rendered')
    if (!modalState) return null;
    return (
        <div className="modal">
            <p>This is my modal!</p>
            <button onClick={() => setModalState(false)}>Close</button>
        </div>
    )
}
export default forwardRef(Modal)

Best Practices:

  1. Limit the number of exposed methods

  2. useImperativeHandle should only be used when necessary. In most cases, it's better to use props and callbacks to communicate between parent and child components.

  3. Avoid circular dependencies: useImperativeHandle can create circular dependencies between parent and child components.

  4. Consider using TypeScript.

Conclusion:

The useImperativeHandle hook allows you to call a function defined in the child component from the parent component, breaking the typical rule of defining functions in the parent component and passing them down. This hook, used in conjunction with the useRef hook and the forwardRef component gives you greater flexibility and control over your components.

Thank you for taking the time to read this blog. I hope it has been informative and has helped you gain a deeper understanding of the topic. If you have any feedback or suggestions for future content, please let me know!