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