A .NET MessageBox-like React component: react-message-box-async
October 25, 2024 • ☕️☕️ 6 min read
If you’ve ever worked with .NET WPF, you’re familiar with the simple message box component it offers. Here’s a basic example:
C# example:
var result = MessageBox.Show("Are you sure?", "Confirmation", MessageBoxButton.YesNo);
if (result == MessageBoxResult.Yes)
{
// Perform the action
}
A similar message box exists even in the older .NET WinForms and probably many other frameworks outside the .NET world. These components allow developers to ask for a quick decision or show a message. Importantly, these components make it easy to control the logic flow in the application based on the user responses. They also allow showing a message while the application is stopped in a specific step of business logic execution.
It’s also worth mentioning the native JavaScript alternatives: alert, confirm and prompt. Developers can use These built-in browser dialogs without needing extra libraries. They are simple, synchronous, and block interaction with the rest of the webpage until the user takes action.
Native JavaScript examples:
alert("This is an alert message!")
if (confirm("Are you sure?")) {
console.log("User clicked OK")
} else {
console.log("User clicked Cancel")
}
var userInput = prompt("Please enter your name:")
if (userInput) {
console.log("Hello, " + userInput)
}
While developing React applications, I sometimes encounter situations where I want to use a similar pattern. That led to the creation of react-message-box-async, which brings the .NET message box style into the React world.
Example of a simple message box with react-message-box-async
:
import { useMessageBox } from "react-message-box-async"
const Example = () => {
const messageBox = useMessageBox()
const handleClick = async () => {
const result = await messageBox.show("Operation finished. Don't ask how.")
if (result === "accept") {
console.log("User accepted")
} else {
console.log("User rejected")
}
}
return <button onClick={handleClick}>Show simple message</button>
}
Criticism of the imperative async approach
One of the main criticisms of this component is that it follows an imperative pattern for rendering. In React, we usually think about components being stateful and declarative. We describe what the UI should look like based on the current state, and React takes care of re-rendering when the state changes.
The imperative approach hides the message box’s open state from the developer. This could lead to confusing behavior, where the message box might close or behave unexpectedly during re-renders.
In the “React way,” we would usually store the message box’s open state in the parent component’s state. Based on this state, a dialog component would be explicitly rendered within the parent’s render function.
Some may question whether waiting for user input with async/await is okay. However, while my first intuition was that this could be problematic, I have yet to find any reasons to avoid this.
Other downsides
Of course, this approach only fits some cases. If you need a more complex dialog, like a settings window with dynamic content, you’re better off using an approach where the state and rendering are explicitly controlled.
When the imperative pattern works
Still, there are cases where this pattern works perfectly fine. For example, for quick notifications like “Are you sure?” or “This operation is not allowed,” this approach can save you a lot of time. You don’t need to build a whole dialog component, manage the state, and add it to the render tree. You call a function, and the message box appears.
Another advantage is when dealing with complicated, multi-level logic, where different messages may need to be displayed at various branches, possibly one after another. The imperative async pattern makes it easier to handle these scenarios without cluttering your components with extra state and logic.
Key features of react-message-box-async
- Simple API: You call an imported function to open the dialog, with no need for explicit dialog components or state handling. The call returns a promise, allowing you to wait for and handle user responses cleanly.
- Customizable: You can customize the title, message, and button options to suit your needs.
- Quick and easy: Ideal for situations where you must show quick alerts or confirmation dialogs and control logic flow based on the responses without extra state management.
Usage example
Here’s how you can use the react-message-box-async
in your React app:
import { useMessageBox } from "react-message-box-async"
const SimpleExample = () => {
const messageBox = useMessageBox()
const handleClick = async () => {
const result = await messageBox.show("Operation completed successfully.")
console.log(result) // "accept" when OK is clicked
}
return <button onClick={handleClick}>Show simple message</button>
}
A more advanced example:
import { useMessageBox } from "react-message-box-async"
const AdvancedExample = () => {
const messageBox = useMessageBox()
const handleClick = async () => {
const result = await messageBox.show({
msg: "Are you sure you want to proceed?",
acceptButton: { content: "Yes, I'm sure", variant: "danger" },
cancelButton: { content: "Cancel", variant: "secondary" },
})
if (result === "accept") {
const secondResult = await messageBox.show({
msg: "Are you really sure?",
acceptButton: { content: "Yes, I'm really sure", variant: "danger" },
cancelButton: { content: "No, I'm not", variant: "secondary" },
})
if (secondResult === "accept") {
console.log("User is really sure")
messageBox.show("Operation completed successfully.")
}
} else {
console.log("User cancelled the action")
}
}
return <button onClick={handleClick}>Go</button>
}
Final thoughts
While the imperative async pattern might not be the best fit for all cases, it can be a useful tool in your React toolbox. It can save you time and make your code cleaner in situations where you need to show quick alerts or confirmation dialogs and control logic flow based on the user’s responses. The react-message-box-async
is available as a npm-package and on GitHub. It provides a simple API for this pattern, allowing you to focus on your application’s logic without worrying about managing dialog state and rendering. The component has some customization options, but if you want other styles or more complex dialogs, or even if you don’t, consider just copying the component to your own project repository, for your customization and dependency-free maintenance. The component is so simple (and the core logic is in one file), dependency free (on purpose), that this should be a breeze.