DEV Community

Cover image for Showing a modal dialog with useImperativeHandle() React hook
Konstantin Tarkus
Konstantin Tarkus

Posted on

4 1

Showing a modal dialog with useImperativeHandle() React hook

Take Material UI's Dialog component as an example that has open: boolean React prop as a way to manage its open/closed state. In Material UI documentation you will find a usage example similar to this:

import * as React from "react";
import { Button, Container, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";

export function Example(): JSX.Element {
  const [open, setOpen] = React.useState(false);
  const handleOpen = React.useCallback(() => setOpen(true), []);
  const handleClose = React.useCallback(() => setOpen(false), []);
  const handleAction = React.useCallback(() => { ... }, []);

  return (
    <Container>
       <Button onClick={handleOpen}>Open Dialog</Button>

       <Dialog open={state.open} onClose={handleClose}>
         <DialogTitle>...</DialogTitle>
        <DialogContent>
          ...
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose}>Cancel</Button>
          <Button onClick={handleAction}>OK</Button>
        </DialogActions>
      </Dialog>
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the original example, the dialog is used in place. Normally, you want to extract dialog in a standalone component, for example:

import * as React from "react";
import { Button, Container, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@mui/material";

export function ConfirmDialog(props: ConfirmDialogProps): JSX.Element {
  const [state, setState] = ...
  const handleClose = ...
  const handleConfirm = ...

  return (
    <Dialog open={state.open} {...props}>
      <DialogTitle>...</DialogTitle>
      <DialogContent>
        ...
      </DialogContent>
      <DialogActions>
          <Button onClick={handleClose}>Cancel</Button>
          <Button onClick={handleConfirm}>OK</Button>
      </DialogActions>
    </Dialog>
  );
}

export type ConfirmDialogProps = Omit<DialogProps, "open">;
Enter fullscreen mode Exit fullscreen mode

Afterwards, the original example could be reduced to the following:

import * as React from "react";
import { ConfirmDialog } from "../dialogs/ConfirmDialog.js";

export function Example(): JSX.Element {
  const handleOpen = ...
  const handleAction = ...

  return (
    <Container>
       <Button onClick={handleOpen}>Open Dialog</Button>
       <ConfirmDialog onConfirm={handleAction} />
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

If the dialog can be used without a need to manage its state in-place that code would look nice and clean.

There are multiple ways to implement it, e.g. by introducing a top-level DialogProvider component + useDialog(...) React hook, alternatively you can add an imperative handler to the dialog itself so that it can be opened using dialogRef.current?.open() method available on the dialog instance.

import * as React from "react";
import { ConfirmDialog } from "../dialogs/ConfirmDialog.js";

export function Example(): JSX.Element {
  const dialogRef = React.useRef<DialogElement>(null);
  const handleOpen = React.useCallback(() = dialogRef.current?.open(), []);
  const handleAction = ...

  return (
    <Container>
       <Button onClick={handleOpen}>Open Dialog</Button>
       <ConfirmDialog ref={dialogRef} onConfirm={handleAction} />
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now let's see how the implementation of this dialog including .open() method implemented with useImeprativeHandle(ref, ...), useState() React hooks looks like:

import * as React from "react";
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@mui/material";

export const ConfirmDialog = React.forwardRef<
  DialogElement,
  ConfirmDialogProps
>(function ConfirmDialog(props, ref): JSX.Element {
  const { onClose, onConfirm, ...other } = props;
  const [state, setState] = React.useState<State>({ open: false });
  const handleClose = useHandleClose(setState, onClose);
  const handleConfirm = useHandleConfirm(setState, onConfirm);

  React.useImperativeHandle(ref, () => ({
    open() {
      setState({ open: true });
    },
  }));

  return (
    <Dialog open={state.open} onClose={handleClose} {...other}>
      <DialogTitle>...</DialogTitle>
      <DialogContent>...</DialogContent>
      <DialogActions>
        <Button onClick={handleClose}>Cancel</Button>
        <Button onClick={handleConfirm}>OK</Button>
      </DialogActions>
    </Dialog>
  );
});

function useHandleClose(setState: SetState, handleClose?: CloseHandler) {
  return React.useCallback<CloseHandler>(function (event, reason) {
    setState({ open: false });
    handleClose?.(event, reason ?? "backdropClick");
  }, []);
}

function useHandleConfirm(setState: SetState, handleConfirm?: ConfirmHandler) {
  return React.useCallback(async function () {
    await handleConfirm?.();
    setState({ open: false });
  }, []);
}

type State = { open: boolean; error?: Error };
type SetState = React.Dispatch<React.SetStateAction<State>>;
type CloseHandler = NonNullable<DialogProps["onClose"]>;
type ConfirmHandler = () => Promise<void> | void;

export type DialogElement = { open: () => void };

export type ConfirmDialogProps = Omit<DialogProps, "open"> & {
  onConfirm?: ConfirmHandler;
};
Enter fullscreen mode Exit fullscreen mode

There are pros and cons of this approach, on the good side is that it's fully self-contained and doesn't rely on any external state management solutions.

https://github.com/kriasoft/react-starter-kit/discussions/2004

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

Top comments (1)

Collapse
 
naucode profile image
Al - Naucode

Great article, keep the good work! Liked and followed! 🚀

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay