Showing a modal dialog with useImperativeHandle() React hook

Showing a modal dialog with useImperativeHandle() React hook

Source Node: 1778496

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> );
}

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">;

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> );
}

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> );
}

Now let’s see how the implementation of this dialog including .open() method implemented with useImeprativeHandle(ref, ...) 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;
};

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

Time Stamp:

More from Codementor React Fact