A solution to handling modals in React

Photo by callmefred

As you might know from my linkedin page, I'm working with Musixmatch to help them deliver the best experience to the users working on the front-end apps they have.
Between the various features I've been requested to implement, I've also been able to put some effort into the improvement of the codebase implementing tests, refactoring code, and so on and so forth.

As soon as I entered I've noticed quite some weird things that I didn't like and I've started presenting some ideas for some of them on how they might change for the better. One of these was the usage of the modals and so I started proposing an abstraction to control them.

Terminology

  • Modal: a white container over a shadowed background
  • Dialog: a component that uses a Modal to display some text and a confirm or deny button

The abstraction

When creating an abstraction for handling modals there are two things you gotta look out for:

  1. How to drive the state which will fill the modal's content and make it visible
  2. What kind of API to expose to the user to display and close a modal

And the following is what we wanted to avoid:

  • Multiple component rendering: a single, but dynamic modal component must be rendered.
  • Verbosity: the implementation should be as less verbose as possible.

1. How to drive the state

Here a list of the things you generally need to store with a dialog, for example:

  • Title
  • Description
  • Buttons' text
  • Buttons' handlers

A very common solution here is to go with a global state manager* (or a context) and put this info in it. Then you create a generic dialog component that subscribes to the state and displays those data. We realized very soon that we weren't looking for a solution that forces us to store the buttons' handlers far away from the rest of the code. Instead, the idea was to colocate them right after the opening of the dialog.

*We use Redux like many of you and the first issue with it is that you can't store functions in it (since it must be serializable all the time). I explored solutions to get around this problem, but they were quite cumbersome.

2. An imperative API

During the process of thinking about the abstraction, we also came across some articles that made us re-think the way the code was written to react to the user's response to our dialogs.
Usually a dialog is used in this way:

const userPressedButton2 = () => {
  setTheSaveThingDialogVisible2(true)
}
// somewhere in the code
const userPressedButton = () => {
  setTheSaveThingDialogVisible1(true)
}
const onOk2 = () => {}
//  many lines after
const onOk1 = () => {}
// many lines after
<Dialog1
  onOkPress={onOk1}
  visible={theSaveThingDialogVisible1}
/>
<Dialog2
  onOkPress={onOk2}
  visible={theSaveThingDialogVisible2}
/>

This might be fine when your components are small and your application too. But storing functions higher in the component and changing the state in order to produce visible side-effects somewhere else in the code were adding too much indirection and were hiding the execution flow of the code not to mention the many states' variables that were popping out to make everything behave accordingly.
In other words, I wanted an "openDialog" function to call with the proper info to show the dialog and to return the user's response sometime in the future: I was looking for a promise.
This is what we're used to calling an imperative approach.

The solution

I present you the useDialog hook:

const {isDialogVisible, openDialog, closeDialog, Dialog} = useDialog()

const userPressedButton = async () => {
  const {res} = await openDialog(Dialog1, optionalNonStaticProps)

  if(res === 'primary) {
    //wathever
  }
}

<Dialog visible={isDialogVisible} />

Here you have it:

  • One component rendered
  • Opening the dialog you can insert an external component where static props are already defined (for static I mean stuff like strings etc. that don't rely on pieces of state in that component)
  • A natural order of execution

Conclusion

Always bear in mind that opening yourself to critiques can only do good for you as long as they're constructive and you keep a positive attitude. In fact, I am super lucky to have such a wonderful environment in Musixmatch with the devs really open to changes and improvements. Also, the workflow is marvelous which allows us to do a team job instead of a lone one thanks to the discussions and the duty of reviewing each other's work. Indeed what I presented you today, is the fruit of our collaboration inside the team and not of my sole work.

Thank you all to the mxm team for the great work and let's keep rocking!