• 752 words

A solution to handling modals in React

I want to show you a new solution for handling modals in large React applications as a result of a big refactor in my work in Musixmatch.

Yellow diverted traffic sign
Photo by Call Me Fred / Unsplash

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.

Specifically, I’m working in the team of the Studio app, which is the app where all the metadata about songs is collected. This application is quite complex for a variety of reasons and among the many struggles the developers are facing, one was the handling of modals: too many state variables, too many modals, and following the logic on when and why they should appear was quite difficult. So I’ve started presenting a few ideas on how this could be improved. More specifically I wanted to create 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

These are the things that generally a Dialog receives:

  • 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 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 App = () => {
  const { isDialogVisible, openDialog, closeDialog, Dialog } = useDialog();

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

    if (res === "primary") {
      //wathever
    }
  };

  return <Dialog visible={isDialogVisible} />;
};

Here you have it:

  • One component rendered
  • Less verbose since you can pass a component in an external file where static props are already applied (by static I mean stuff that don’t rely on the state)
  • 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.

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