DEV Community

Cover image for The neatest way to handle alert dialogs in React 🥰
Dmitriy Kovalenko
Dmitriy Kovalenko

Posted on

The neatest way to handle alert dialogs in React 🥰

Time to read — 5 mins ☕️

Hola! Lazy dev here and we will talk about handling dialog alerts in react without tears 😢. If you are tired of tons of copy-pastes just to create new freaking «one question» modal dialog — prepare your coffee we are starting.

The goal

We want to make the neatest solution for displaying an alert. Pretty similar to what we have in a browser with a native alert function.



const isConfirmed = alert("Are you sure you want to remove this burrito?");

if (isConfirmed) {
  await api.deleteThisAwfulBurrito();
}


Enter fullscreen mode Exit fullscreen mode

Sneak peek

Finally we will get to something like this.



const YourAwesomeComponent = () => {
  const confirm = useConfirmation()

  confirm({
    variant: "danger",
    title: "Are you sure you want to remove this burrito?",
    description: "If you will remove this burrito you will regret it 😡!!"
  }).then(() => {
    api.deleteThisAwfulBurrito();
  });
}



Enter fullscreen mode Exit fullscreen mode

Interested? Let's write some code.

First of all, we need to start with creating actually the modal dialog. This is just a simple alert dialog built with ❤️ and material-ui



import {
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
} from "@material-ui/core";

export const ConfirmationDialog = ({
  open,
  title,
  variant,
  description,
  onSubmit,
  onClose
}) => {
  return (
    <Dialog open={open}>
      <DialogTitle>{title}</DialogTitle>
      <DialogContent>
        <DialogContentText>{description}</DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button color="primary" onClick={onSubmit}>
          YES, I AGREE
        </Button>
        <Button color="primary" onClick={onClose} autoFocus>
          CANCEL
        </Button>
      </DialogActions>
    </Dialog>
  );
};



Enter fullscreen mode Exit fullscreen mode

OK, but how we will adopt it to be working dynamically? That's an interesting thing to consider. Why do we need a lot of dialogs for each component if the user can see only one alert dialog simultaneously?

If you are showing an alert dialog over the other alert dialog...you probably need to reconsider the UX part of your application.
No god please no image

So here we go. Everything we need is to render only 1 top-level modal at the root of our application and show it when we need to. We'll use the power of react hooks to make it looks gracefully.

Wrap the context

Let's create a new context instance and wrap our component tree with it. Also, create a simple state that will save the currently displaying options for the alert (like title, description and everything you need).



interface ConfirmationOptions {
  title: string;
  description: string;
}

const ConfirmationServiceContext = React.createContext<
  // we will pass the openning dialog function directly to consumers
  (options: ConfirmationOptions) => Promise<void>
>(Promise.reject);

export const ConfirmationServiceProvider= ({ children }) => {
  const [
    confirmationState,
    setConfirmationState
  ] = React.useState<ConfirmationOptions | null>(null);

  const openConfirmation = (options: ConfirmationOptions) => {
    setConfirmationState(options);
    return Promise.resolve()
  };

  return (
    <>
      <ConfirmationServiceContext.Provider
        value={openConfirmation}
        children={children}
      />

      <Dialog open={Boolean(confirmationState)} {...confirmationState} />
    </>
  );
};



Enter fullscreen mode Exit fullscreen mode

Now our dialog will be opened once we connect any consumer and call the provided function.

Resolve confirmation

And now we need to somehow deal with closing dialog and getting a callback from the consumers. Here was used Promise based API, but it is possible to make it works using a callback style. In this example, once the user accepted or canceled the alert, your awaiting promise will be resolved or rejected.

To do so we need to save Promise's resolving functions and call them on appropriate user action. React's ref is the best place for that.



  const awaitingPromiseRef = React.useRef<{
    resolve: () => void;
    reject: () => void;
  }>();

  const openConfirmation = (options: ConfirmationOptions) => {
    setConfirmationState(options);
    return new Promise((resolve, reject) => {
      // save the promise result to the ref
      awaitingPromiseRef.current = { resolve, reject };
    });
  };

  const handleClose = () => {
    // Mostly always you don't need to handle canceling of alert dialog
    // So shutting up the unhandledPromiseRejection errors 
    if (confirmationState.catchOnCancel && awaitingPromiseRef.current) {
      awaitingPromiseRef.current.reject();
    }

    setConfirmationState(null);
  };

  const handleSubmit = () => {
    if (awaitingPromiseRef.current) {
      awaitingPromiseRef.current.resolve();
    }

    setConfirmationState(null);
  };



Enter fullscreen mode Exit fullscreen mode

That's it! Our dialog machine is almost ready! One thing is left — create a custom hook for better readability



export const useConfirmationService = () =>
  React.useContext(ConfirmationServiceContext);


Enter fullscreen mode Exit fullscreen mode

Customization

You can easily customize dialog content by passing additional variant prop. Just add it to the ConfirmationOptions



export interface ConfirmationOptions {
  variant: "danger" | "info";
  title: string;
  description: string;
}



Enter fullscreen mode Exit fullscreen mode

And render different dialog content as you wish.



<DialogActions>
{variant === "danger" && (
<>
<Button color="primary" onClick={onSubmit}>
Yes, I agree
</Button>
<Button color="primary" onClick={onClose} autoFocus>
CANCEL
</Button>
</>
)}

<span class="si">{</span><span class="nx">variant</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">info</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">Button</span> <span class="na">color</span><span class="p">=</span><span class="s">"primary"</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">onSubmit</span><span class="si">}</span><span class="p">&gt;</span>
    OK
  <span class="p">&lt;/</span><span class="nc">Button</span><span class="p">&gt;</span>
<span class="p">)</span><span class="si">}</span>
Enter fullscreen mode Exit fullscreen mode

</DialogActions>

Enter fullscreen mode Exit fullscreen mode




Are you ready?!

Are you ready kids?!

Here is the final working example. Feel free to just steal the implementation of ConfirmationService.tsx file if you want to. This is pretty standalone and isolated logic of what we were talking about.

P.S. No burritos were harmed in the making of this article

Top comments (25)

Collapse
 
devhammed profile image
Hammed Oyedele • Edited

Nice!!!

I have used this technique for 2 past React projects before ✌️✌️✌️

I used a global hook state manager that I created (github.com/devhammed/use-global-hook) then renders my custom Material UI Dialog component near routes in App.js then all I have to do is to call the showModal action (I use React.useReducer) whenever I need to:

showModal({
  title: "Hello",
  content: (<Typography>Hi</Typography>),
  actions: [] // buttons
})
Collapse
 
dmtrkovalenko profile image
Dmitriy Kovalenko

Cool!

Collapse
 
tabrez96 profile image
Tabrez Basha

Cool, can you share some examples for your implementation?

Collapse
 
devhammed profile image
Hammed Oyedele

The implementation is almost the same with the one in this post but I used use-global-hook instead of context (though it uses context under the hood too).

Collapse
 
cargallo profile image
cargallo

Hi there! Nice solution. It throws an error when you click in either cancelo or accept button of the dialog...

proxyConsole.js:64 Warning: Failed prop type: The prop children is marked as required in ForwardRef(DialogTitle), but its value is undefined.
in ForwardRef(DialogTitle) (created by WithStyles(ForwardRef(DialogTitle)))
in WithStyles(ForwardRef(DialogTitle)) (at ConfirmationDialog.tsx:34)
in div (created by ForwardRef(Paper))
in ForwardRef(Paper) (created by WithStyles(ForwardRef(Paper)))
in WithStyles(ForwardRef(Paper)) (created by ForwardRef(Dialog))
in div (created by Transition)
in Transition (created by ForwardRef(Fade))
in ForwardRef(Fade) (created by TrapFocus)
in TrapFocus (created by ForwardRef(Modal))
in div (created by ForwardRef(Modal))
in ForwardRef(Portal) (created by ForwardRef(Modal))
in ForwardRef(Modal) (created by ForwardRef(Dialog))
in ForwardRef(Dialog) (created by WithStyles(ForwardRef(Dialog)))....

Collapse
 
cargallo profile image
cargallo • Edited

Auto answering this.... changing this line solves the problem.

<DialogTitle id="alert-dialog-title">{title ? title : ""}</DialogTitle>
Collapse
 
onderonur profile image
Onur Önder

This is actually a really cool way to handle confirmations. The only minor problem is, when you press "agree" or "cancel", dialog content disappears just before the exit animation of the dialog completes. It creates some sort of a flickering effect. You need to watch carefully to see it, but if your exit animation duration is higher, you will end up with an empty dialog slowly fading away :)

Other than that, real cool way for confirmations and simple info dialogs!

Collapse
 
sarat12 profile image
Taras Dymkar

So here is the fix of "flickering" effect which was caused by resetting the state before dialog is faded out.

Collapse
 
sarat12 profile image
Taras Dymkar

Yeah, I'm not sure but looks like to solve this issue you need to reset confirmationState on material-ui`s Dialog onExited callback. And on close/submit just set "open" prop to false.

Collapse
 
solflare profile image
solflare • Edited

I know it's a bit late but thanks for the post, I found it really helpful! I was wondering whether or not using this service across multiple components would trigger a rerender for any component using the context any time a component called the confirm function and it seemed like it did. I added a useCallback around the openConfirmation declaration (with a setConfirmationState dependency) and it seemed to do the trick. Can anyone verify that this is the right way to go about this?

Collapse
 
jonlauridsen profile image
Jon Lauridsen

Thanks, clear and concise. I’d like to try this.

Collapse
 
yanisouth profile image
YaniSouth • Edited

Hi, I did a copy of same code but I m getting this error, I hope you can help me.

( this doesn t allow me to upload image WHY GOD WHY?)

C:/Projects/yan/yan-frontend/src/components/Modals/ConfirmationService.tsx
TypeScript error in C:/Projects/malachite/malachite-frontend/src/components/Modals/ConfirmationService.tsx(52,8):
Type '{ open: boolean; onSubmit: () => void; onClose: () => void; } | { catchOnCancel?: boolean | undefined; variant: "danger" | "info"; title: string; description: string; open: boolean; onSubmit: () => void; onClose: () => void; }' is not assignable to type 'IntrinsicAttributes & ConfirmationDialogProps & { children?: ReactNode; }'.
Type '{ open: boolean; onSubmit: () => void; onClose: () => void; }' is missing the following properties from type 'ConfirmationDialogProps': variant, title, description TS2322

50 |       />
51 | 

52 | <ConfirmationDialog
| ^
53 | open={Boolean(confirmationState)}
54 | onSubmit={handleSubmit}
55 | onClose={handleClose}

Collapse
 
nrudolph profile image
Nolan Rudolph

I literally just made an account to say you're incredible, and your tutorial is flawless. Thank you <3

Collapse
 
sirmd profile image
sirmd

Hello! If I try to nest the children props inside the Provider instead of passing it as a prop it return a TypeError: PromiseReject called on non-object. Anyone have an idea why??

Collapse
 
tabrez96 profile image
Tabrez Basha

Is it possible to dynamically change the content of the dialog?
Example sandbox (will be ugly, but just for an example): codesandbox.io/s/neat-dialogs-fork...

Basically, I would like to know if this kind of API would suit for a proper Modal.

Collapse
 
louisshe profile image
Chenglu

This use react hook and does that mean we can only use this in function component not class component?

Collapse
 
dmj2x profile image
David N

Did you ever find out?

Collapse
 
solflare profile image
solflare

Just in case you're still wondering, class components can't use hooks.