Streamlining Modal Dialogs in React
Dialogs are integral components of modern applications, serving as a means for developers to convey intermediate information to users without disrupting the flow. Despite their utility, integrating them into frontend workflows, particularly with React and MUI, can prove surprisingly complex. This article aims to explore the shortcomings of the current implementation of React MUI dialogs and propose a vision for aligning them more closely with the functionality of core JavaScript libraries.
Why?
To gain a deeper comprehension of the complexity involved, let's delve into the code.
To enable MUI dialogs functionality, one must include a lot of imports and manually handle the dialog's open state.
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open alert dialog
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Use Google's location service?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Disagree</Button>
<Button onClick={handleClose} autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}
While the built-in JavaScript methods for alert, confirm, and prompt offer simplicity, they're not customizable and may fall short for complex scenarios.
alert('This is a test');
MUI Dialogs lack the simplicity and functionality of built-in JavaScript methods like alert, confirm, and prompt. I aim to create a user-friendly solution that meets both ease-of-use and complex use case requirements.
Although Mui dialogs are highly customizable by default, they can create significant hurdles for users. Managing states, setting up component skeletons, and handling dialog events becomes cumbersome. I want a solution that is easy to set up, caters to basic needs without excessive customization. Simple functionalities like alert, confirm, and prompt cover most use modal dialog cases. Adding a touch of customization would be ideal for a satisfactory user experience.
Introducing react-dialog-mui
I've developed a simple solution called react-dialog-mui. While there's still work to be done, I demonstrate how easy it can be to achieve this using React context to manage dialog states and hooks to simplify API construction.
This library aims to streamline and unify typical modal use cases, including simple message modals, yes/no confirmation modals, basic input prompt modals, and customizable complex modals.
Getting Started with react-dialog-mui
Follow the instruction on the README file to get started. Basically it involves installing the package (official npm package will be coming soon).
Install the npm package with either npm or yarn
npm install --save react-dialog-mui@^0.1.1
yarn add react-dialog-mui@^0.1.1
Then wrap your application with the ActionDialogsContext.
// then wrap your app with with the ActionDialogContext
import { ActionDialogsContext } from 'react-dialog-mui';
function YourApp() {
return (
<ActionDialogsContext>
<YourAppComponent />
</ActionDialogsContext>
);
}
Below are a list of supported modal types
Alert Dialog
// Useful for displaying messages.
import React from 'react';
import { useActionDialogs } from 'react-dialog-mui';
export function AlertExample() {
const { alert } = useActionDialogs();
const onSubmit = async () => {
try {
await alert({
title: <>Query Result</>,
message: <>The query has successfully executed, yielding 200 records in 15 seconds.</>,
yesLabel: `Acknowledge`,
});
} catch (err) {}
};
return <button onClick={onSubmit}>My Action</button>;
}
Confirmation Dialog
// Useful for confirming user actions.
import React, { useState } from 'react';
import { useActionDialogs } from 'react-dialog-mui';
export function ConfirmExample() {
const { confirm } = useActionDialogs();
const [deleted, setDeleted] = useState(false);
const onSubmit = async () => {
try {
await confirm({
title: <>Confirmation?</>,
message: <>Do you want to delete this query?</>,
yesLabel: `Delete`,
});
// when user selects yes
setDeleted(true);
} catch (err) {
// when user selects no
setDeleted(false);
}
};
return (
<>
<button onClick={onSubmit}>Delete Query?</button>
<div>
Status:
{deleted ? <>This query has been deleted</> : null}
</div>
</>
);
}
Prompt Dialog
// Useful for prompting short text input.
import React, { useState } from 'react';
import { useActionDialogs } from 'react-dialog-mui';
export function PromptExample() {
const { prompt } = useActionDialogs();
const [name, setName] = useState('');
const onSubmit = async () => {
try {
const newName = await prompt({
title: 'Rename Query',
message: 'New Query Name',
value: name,
saveLabel: 'Save',
});
// when user entered and submitted the value for new name
setName(newName);
} catch (err) {}
};
return (
<>
<button onClick={onSubmit}>Rename Query?</button>
<div>
<strong>New query name:</strong> {name}
</div>
</>
);
}
Single-Select Choice Dialog
// Useful for single-select option lists.
import React, { useState } from 'react';
import { useActionDialogs } from 'react-dialog-mui';
export function SingleSelectChoiceExample() {
const { choiceSingle } = useActionDialogs();
const [session, setSession] = useState('');
const onSubmit = async () => {
try {
const newSession = await choiceSingle({
title: 'Switch session', // the dialog title
message: 'Select one of the following sessions:', // the question for the input
options: [
{ label: 'Session 1', value: 'session_1' },
{ label: 'Session 2', value: 'session_2' },
{ label: 'Session 3', value: 'session_3' },
{ label: 'Session 4', value: 'session_4', disabled: true },
],
value: session,
required: true,
});
// when user selected a choice
setSession(newSession);
} catch (err) {
setSession('');
}
};
return (
<>
<button onClick={onSubmit}>Switch Session</button>
<div>
<strong>New selected session:</strong> {session}
</div>
</>
);
}
Multi-Select Choice Dialog
import React, { useState } from 'react';
import { useActionDialogs } from 'react-dialog-mui';
export function MultiSelectChoiceExample() {
const { choiceMultiple } = useActionDialogs();
const [favContacts, setFavContacts] = useState<string[]>([]);
const onSubmit = async () => {
try {
const newFavContacts = await choiceMultiple({
title: 'Update Favorite Contacts',
message: 'Select contacts to add to the favorite list:',
options: [
{ label: 'John Doe', value: 'John Doe' },
{ label: 'Alice Smith', value: 'Alice Smith' },
{ label: 'Michael Johnson', value: 'Michael Johnson', disabled: true },
{ label: 'Emily Brown', value: 'Emily Brown' },
{ label: 'Daniel Wilson', value: 'Daniel Wilson' },
],
value: favContacts,
required: true,
});
// when user selected a choice
setFavContacts(newFavContacts);
} catch (err) {
setFavContacts([]);
}
};
return (
<>
<button onClick={onSubmit}>Update Favorite Contacts</button>
<div>
<strong>New selected favorite contacts:</strong> {JSON.stringify(favContacts)}
</div>
</>
);
}
Custom Modal
// Useful for displaying custom modals.
import React from 'react';
import { useActionDialogs } from 'react-dialog-mui';
function ModalExample() {
const { modal } = useActionDialogs();
const onSubmit = async () => {
try {
await modal({
title: 'Query Details',
message: (
<>
<div>
<strong>Name:</strong> Sample Mocked Query
</div>
<div>
<strong>Status:</strong> Pending
</div>
<div>
<strong>Created Date:</strong> {new Date().toLocaleDateString()}
</div>
</>
),
});
// when users close out of modal
} catch (err) {}
};
return (
<>
<button onClick={onSubmit}>Show Details</button>
</>
);
}
Reference your own component
import React from 'react';
import { useActionDialogs } from 'react-dialog-mui';
function MyChildComponent() {
return (
<>
<div>Hello world</div>
</>
);
}
export function ModalExampleWithChildComponent() {
const { modal } = useActionDialogs();
const onSubmit = async () => {
try {
await modal({
title: 'Simple Modal',
message: <MyChildComponent />,
modalRef: modalRef,
size: 'sm',
});
// when users close out of modal
} catch (err) {}
};
return (
<>
<button onClick={onSubmit}>Show Modal</button>
</>
);
}
Dismiss Custom Modal
For custom modals, manual dismissal post-action, like form submission or interactions, is crucial. This can be achieved via useActionDialogRef and dismiss. Here's a sample code snippet.
Dismiss via button click
import React from 'react';
import { useActionDialogRef, useActionDialogs } from 'react-dialog-mui';
export function ModalExampleWithManualDismiss() {
const { modal } = useActionDialogs();
const modalRef = useActionDialogRef();
const onSubmit = async () => {
try {
await modal({
title: 'Manual Dismiss Modal',
message: (
<>
<div>
<button onClick={() => modalRef.current.dismiss()}>
Manually dismiss this dialog
</button>
</div>
</>
),
modalRef: modalRef,
size: 'sm',
});
// when users close out of modal
} catch (err) {}
};
return (
<>
<button onClick={onSubmit}>Show Modal</button>
</>
);
}
Dismiss via form submission
import React from 'react';
import { useActionDialogRef, useActionDialogs } from 'react-dialog-mui';
export function ModalExampleWithFormSubmit() {
const { modal } = useActionDialogs();
const modalRef = useActionDialogRef();
const onSubmit = async () => {
try {
await modal({
title: 'Login Modal',
message: (
<form
onSubmit={(e) => {
e.preventDefault();
modalRef.current.dismiss();
}}
style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<input type='text' placeholder='Username' required />
<input type='password' placeholder='Password' required />
<div>
<button type='submit'>Login</button>
</div>
</form>
),
modalRef: modalRef,
size: 'sm',
});
// when users close out of modal
} catch (err) {}
};
return (
<>
<button onClick={onSubmit}>Show Modal</button>
</>
);
}
Summary
While Mui dialogs offer extensive customization, they impose significant overhead on users. Managing states, setting up component skeletons, and handling dialog events become tedious tasks. My aim is to simplify this process by offering a straightforward solution that caters to basic needs without extensive customization. Simple setups for common modal types like alert, confirm, and prompt are my priority, with added flexibility for basic customization.
Though not flawless, this solution significantly reduced my workload during the development of my other project, sqlui-native. It minimizes the boilerplate required to create modal dialogs in MUI. The idea is to emulate the functionality of native JavaScript methods for alerting, confirming, and prompting. While currently integrated with React and MUI, this concept can be adapted to other frameworks like Angular or Svelte.