React: Custom Instance Value with useImperativeHandle() Hook

Scenario for Implementation

In React, the primary interface of exchanging data among components is via Props. We can attach objects as properties (commonly known as “props”) from parent to child. The props are non-serializable, that means it can also contain functions or instances.

type Props = {
  name: string,
  onClick: () => void,
};

function Button(props: Props): JSX.Element {
  return <button onClick={props.onClick}>{props.name}</button>;
}

Additionally, React can provide an instance of a component via ref. When we set ref of an HTMLElement, the current value of the ref will contain the DOM instance of that element. We can also attach ref in our custom components -

const Input = React.forwardRef((props, ref): JSX.Element => (
  <input ref={ref} />
));

const inputRef = React.createRef();
<Input ref={inputRef} />;

Now, inputRef will have an instance of input DOM Element. If we want to focus on the input programmatically, we can call inputRef.current?.focus().

It’s convenient in many cases when we have to update the state related to the component as a side-effect from another action. For example, we need to show alert before updating or removing an entity.

Let’s assume, we have a component for showing alert and it has the following props message, onSuccess, and onClose. We can create a state that holds all the props and pass that to a common <Alert /> component.

type AlertProps = {
 message: string;
 onSuccess: () => void;
 onClose: () => void;
}

function App() {
 const [props, setProps] = React.useState<AlertProps>();
 return (
   <>
     {props && <Alert {...props} />}
     <button
       onClick={() => setProps({
         message: "Message 1",
         onSuccess: () => alert("Action 1"),
         onClose: () => setProps(undefined),
       })}
     >Show 1</button>
     <button
       onClick={() => setProps({
         message: "Message 2",
         onSuccess: () => alert("Action 2"),
         onClose: () => setProps(undefined),
       })}
     >Show 2</button>
   </>
 );
}

The code above will work perfectly, but there is one problem - When a parent component re-renders, the child components will be re-rendered forcefully.

In the worst-case scenario, it will trigger a re-render of the parent component with very heavy child computation just for showing a modal.

That is where a modified handle of the instance can be very helpful - when the state/props changes of the component will be done programmatically most of the time. And we can do exactly that using the useImperativeHandle() hook. This is directly from React’s documentation -

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef.

Implementation

So, let’s create an <Alert /> component with a custom instance.

// File: alert.component.tsx
import React from "react";
import Modal from "react-modal";

type AlertData = {
  msg?: string;
  onSuccess?: Function;
};

export type AlertInstance = {
  open: (alertData: AlertData) => void;
  close: () => void;
};

const customStyles = {
  content: {
    top: "10%",
    left: "50%",
    right: "auto",
    bottom: "auto",
    marginRight: "-50%",
    transform: "translate(-50%, -50%)",
  },
};

const AlertComponent = React.forwardRef<{}, {}>((props, ref) => {
  const [data, setData] = React.useState<AlertData>();
  const [modalIsOpen, setIsOpen] = React.useState(false);
  const closeAlert = React.useCallback(() => setIsOpen(false), []);
  React.useImperativeHandle(
    ref,
    (): AlertInstance => ({
      open: (data) => {
        setData(data);
        setIsOpen(true);
      },
      close: closeAlert,
    }),
    [closeAlert]
  );
  return (
    <Modal
      isOpen={modalIsOpen}
      style={customStyles}
      contentLabel="Example Modal"
    >
      <b>{data?.msg ?? "Are you sure you want to continue?"}</b>
      <div className="btn-row default-margin">
        <button
          onClick={() => {
            closeAlert();
            data?.onSuccess?.();
          }}
        >
          OK
        </button>
        <button onClick={closeAlert}>Cancel</button>
      </div>
    </Modal>
  );
});

export const Alert = React.memo(AlertComponent);

Here, AlertInstance is the custom instance that will accessible in the ref container of the <Alert /> component. useImperativeHandle() accepts three parameters -

  1. Forwarded ref of the component
  2. Initialization function that will create the instance
  3. Dependency Array for recomputing the instance

Now, we can use the alert component in other components and access its open() and close() action programmatically.

import React from "react";
import { Alert, AlertInstance } from "./alert.component";

function App() {
  const alertModal = (React.useRef < AlertInstance) | (null > null);
  return (
    <div>
      <h1>
        DemoApp: <b>useImperativeHandle()</b> Hook
      </h1>
      <button
        onClick={() => {
          // Open alert
          alertModal.current?.open({
            msg: "Alert from parent",
            onSuccess: () => alert("Success"),
          });
          // Close alert modal after 2 seconds
          setTimeout(() => alertModal.current?.close(), 2000);
        }}
      >
        Open Alert
      </button>
      <Alert ref={alertModal} />
    </div>
  );
}

export default App;

You can find the full project here in my GitHub repository.

Profiling Results

Prop based Alert

prop-based-alert-profile-result

Custom instance based Alert

custom-instance-based-alert-profile-result

References

  1. React: ref
  2. React: useImperativeHandle
  3. React: Forwarding Refs
  4. React Modal
  5. Full Demo Project