Skip to content

Fixing React Promise Modals — The Nice and The Proper

Author's photo
6 min read ·

Continuing on my previous post on how we have built @prezly/react-promise-modal at Prezly. In this article I describe the problems we've faced with the initial implementation and how we've fixed them in the second version.

Image showing the name and the stats for the @prezly/react-promise-modal Github package.

TLDR: Check the v2 version usage examples as well as the code sandbox.

The Nice

The initial version, though being super nice to use, had nasty issues. All of them coming from the fact that we were playing against React's declarative/reactive approach by making the function imperative, fully disconnected from the React rendering lifecycle.

  1. Modal's contents were lacking reactivity Once rendered, the modal contents were never updated. It may have been unimportant for simple cases, but became an issue when we wanted the modal contents to react to external data updates.

  2. Modals were rendered outside the current React rendering root Which initially seemed like a blessing, turned out to be a serious problem later. The most obvious consequence was the inability to access React context values.

    We've noticed this when we tried to render dates in a modal. In Prezly users can set their preferred date format in settings. The app provides user settings through React context, allowing underlying components to render accordingly.

    We've also received a couple of bug reports related to this:

  3. Modals were fully disconnected from the component's rendering lifecycle

    Once spawned, the caller component had very little control over the modal. The modal could even stay on the screen after the caller component had been already unmounted.

    Some people had problems with this not playing well with hot module reloading:

So in the end it was clear, that even though the initial implementation looked super nice and sexy, it was completely flawed. This is what you get by playing against React. Lesson learned!

The Proper

For a while we were still using the flawed implementation, carefully working around its limitations. The most annoying was passing the app context again into every modal that used components depending on user settings, or a different context provider.

Finally, in December 2022 I felt it was time to fix it!

Historically December was always quiet at Prezly. With many people from the team being on Christmas holidays, as well as the majority of our users. It's a nice time to slow down a little bit and focus on lower priority tasks problems that were itching the hardest.

The requirements I had in mind for the second iteration:

  • I wanted to keep the invocation API on the same level of simplicity: call it, get a promise that resolves when the modal closes. Still sounds good.

  • It has to be a part of the component rendering hierarchy. This way it will automatically inherit all context values the caller component is rendered with.

  • It has to be rendered declaratively, reactively responding to incoming props changes and state updates.

Since the initial implementation was built the React team has released a few major updates, apart from other changes, introducing React hooks. Though being confusing to newcomers, hooks are perfect for composability — abstractions done right are enjoyable to use. So it was a natural decision to build it as a hook.

After trying a few different approaches I've settled with this approach:

// 1) Define your modal
const confirmation = usePromiseModal(
    (props) => <ConfirmationModal {...props} />,
);

// 2) Call it in your event handler
async function handleClick() {
    // 3) Wait for the modal to resolve
    if (await confirmation.invoke({ title: 'Are you sure?' })) {
        // TODO: Perform the operation.
    }
}

The beauty of it is that it's hybrid, providing benefits of both worlds:

  • the modal rendering is defined declaratively using a hook call, allowing for reactivity to declare-time variables (component state and incoming props)

  • the modal invocation is imperative, just like before, and still allows passing additional call-time arguments.

The hook is a different, still elegant abstraction. It's a building block, allowing you to build any transactional modal window interaction: alert(), confirm(), and prompt().

We've been successfully using this approach at Prezly for the last two years. It has proven its robustness, while being free from the flaws of the initial implementation.

This December I've taken time to extract the hook from the Prezly CRM application internal code. It's now available to everyone as v2 of the original package: @prezly/react-promise-modal

Check the Usage examples as well as the code sandbox.

Closing Thoughts

Playing against the React's declarative paradigm can sometimes be beneficial for performance gains or nicer abstractions. But only do this being fully aware of what's going on, and for a damn good reason.

Let me know what you think!

Cheers! 🖖

End of article
Got any comments?