Supporting Multiple Modals in React Native: A New Approach
Managing multiple modals is a limitation seen in both React Native’s standard modal implementation and in popular third-party libraries. Learn how we came up with our own approach to address this, and how to use our rn-modal-presenter library.
by the Whitespectre React Native Team
This article was co-authored by Lucas Diez de Medina and Rui Lu, members of the Whitespectre React Native team.
During an investigation for an app build, we noticed that the current React Native standard modal implementation is unable to manage multiple modals presented at the same time. Popular third-party libraries out there didn’t work for us either, as they presented a similar limitation. And the more complex your React Native app, the more limitations you run into. What’s worse, errors can occur unnoticed.
So we decided to come up with our own simple approach and launch our rn-modal-presenter library, which allows you to present multiple modals on top of your app’s content. Read on to learn about our findings and how to use our library for your own React Native project.
In this article we’ll cover:
- Limitations of third-party modal libraries
- Alternatives tested
- Our solution: rn-modal-presenter library
- How to implement the rn-modal-presenter library
Read the full rn-modal-presenter library documentation on npm and GitHub.
Limitations of Third-Party Modal Libraries
Now as we mentioned above, as crucial as modals are on React Native, the standard implementation presents some significant limitations. This is something we discovered when attempting to show multiple modals to users under different circumstances.
To elaborate, modals are components that are shown/hidden based on a prop, which is usually controlled by the state of the presenting component. The component must be part of the component tree and appear as a child of any component that wants to present it. Considering that, fundamental limitations and complications arise when you want to show multiple modals presented by one single component or when you need to navigate to a separate screen with a modal being presented.
When trying to do so:
- You’ll need to manage the state that controls each of the modals being presented.
- By updating the state, you have to handle the presentation and dismissal of the modals all in the same place (the presenting component).
- If you want to navigate back after presenting the modal from a component, because the presenting component is unmounted the modal also disappears.
- On iOS, you can’t present more than one modal simultaneously or stack multiple modals shown on top of each other: https://github.com/react-native-modal/react-native-modal/issues/30
On the last point, the limitation is on the iOS native side. If a UIViewController presents another view controller on top of it, then the presenting view controller (which is now hidden) cannot present another second view controller on top of that one. So you need to show the second one from the already presented view controller.
A Quick Example
Let’s see these limitations in action with a simple example where we attempt to include the classic modal to rate the app, but only show it to satisfied users under given circumstances.
After the user performs a relevant task in the app, we show them:
- A modal indicating that the task was completed.
- A modal asking them how happy they feel about the app
- If the answer is positive, we show them a modal prompting them to rate the app on the AppStore
- If the answer is negative, we offer them a modal asking them to leave us feedback
So all these modals are presented from the same main screen, and, as you can see in this code snippet, we have all the states for the four different modals and then we show each of those modals based on the state.
const App = () => {
const [showRateAppModal, setShowRateAppModal] = useState(false);
const [showAppRatedPositiveModal, setShowAppRatedPositiveModal] = useState(false);
const [showAppRatedNegativeModal, setShowAppRatedNegativeModal] = useState(false);
const [showActivateGadgetModal, setShowActivateGadgetModal] = useState(false);
return (
<SafeAreaView>
{showActivateGadgetModal && (
<ActivateGadgetModal
onDismiss={() => {
setShowActivateGadgetModal(false);
}}
/>
)}
{showRateAppModal && (
<RateAppModal
positiveFeedback={() => {
setShowRateAppModal(false);
setShowAppRatedPositiveModal(true);
}}
negativeFeedback={() => {
setShowRateAppModal(false);
setShowAppRatedNegativeModal(true);
}}
/>
)}
{showAppRatedPositiveModal && (
<PositiveFeedbackModal
onDismiss={() => setShowAppRatedPositiveModal(false)}
/>
)}
{showAppRatedNegativeModal && (
<NegativeFeedbackModal
onDismiss={() => setShowAppRatedNegativeModal(false)}
/>
)}
<Button
title="Activate Gadget"
onPress={() => {
setShowActivateGadgetModal(true);
setShowRateAppModal(true);
}}
/>
</SafeAreaView>
);
};
This interaction is pretty simple because we control the states from the same component, and we don’t have a store. Yet you’re already managing a store with 4 state variables, which becomes more complex as you keep adding to it.
Now let’s see what happens if, by mistake, you try to present two modals simultaneously.
In this case, the first modal will be shown on iOS only, and the worst part of this is that as far as React Native’s console is concerned, no error has happened.
However, checking the console in Xcode will tell you that you are trying to show a View Controller on top of another View Controller, which is not correct behavior for the iOS system.
The error you will see looks like this:
Warning: Attempt to present <UIViewController: 0x147d2c6b0> on <UIViewController: 0x147d614c0> which is already presenting (null)
The correct flow for iOS would be to show the first modal and then, once the first modal is dismissed (by setting the corresponding state variable to false
), set the next modal’s state variable to true
to show it.
As you can imagine, this can become very messy, and in many cases developers use delays or timeouts to manage the dismissal and presentation of multiple modals. This leads to unexpected failures and in practice, the modal animation durations can vary depending on the device the app is running on.
To summarize, the problem we face here is that the presenting component must handle all the logic and the state management; and we need one state variable per modal we want to show.
And the biggest problem for us is that if we make a single mistake, like trying to present two modals at once, the second one doesn’t appear and we don’t get any errors/warnings.
Alternatives Tested
The 2 common issues you will face with managing multiple modals in your React Native project:
- the inability to present more than one modal at the same time
- the complexity of your code grows exponentially as you add more and more modals
Faced with these two problems with our Standard Native Modal component, we tried different options to solve this.
Below are 2 of the most widely used libraries down here and explain why, and ultimately, why we decided to go our own way.
react-native-modal
This is an extension of the Standard React Native Modal Component because it provides additional features to the existing ones, like specifying enter/exit animation timing, providing other callbacks to customize the APIs, and swipeable, scrollable, and adaptive content to the device orientation.
However good those features are, this library doesn’t ultimately change how modals work in React Native, so it still has the same limitations mentioned above, like presenting multiple modals at the same time or reducing the code complexity.
react-native-modalfy
Even though this library is way less popular than the previous one, its scope is in the direction of what we want to achieve.
The first benefit is that it supports multiple modals as it is implemented in JavaScript. You have to call a JavaScript function from anywhere in the code, without having to mix up your component tree and manage extra states.
The other important thing is that it is based on imperative APIs, and that you can fully customize the animation and the transition for each modal appearance.
Despite these benefits, this library required boilerplate code to set it up. In that sense, it’s very similar to React Navigation, where you need to define the routes to register every modal you have in your project beforehand and provide configurations for each of them.
The other setback is that it doesn’t allow us to show/hide multiple instances of the same modal type. In an app, where we have numerous modals with different buttons and copies that share the same styling, it’s more efficient to have only one modal with specific parameters for different copies instead of creating a lot of different but very similar modal entities.
Our solution: rn-modal-presenter
After analyzing many options, we decided to create our own library based on the following foundations:
Flexible component and Imperative API
Flexible in the sense that you can present any component as a modal, and you can manage the presentation/dismissal from anywhere in the code (including from within the presented modal) without modifying the state.
The Imperative API will allow you to trigger modal presentations from anywhere, since the modals are presented on top of the whole app instead of on top of a specific screen/component.
The content is presented on top of the parent component (usually your component)
This parent component is exposed from our library, and you need to place it somewhere in your component tree. This is cool because the modals have the highest priorities against your other regular views and screens.
Multiple modals support
As this is a 100% JavaScript library, showing multiple modals is supported out of the box. And also we do support showing multiple instances of the same modal type.
This poses, however, a mild limitation, which is that as it is a JavaScript solution, if you have some other Native modals in your project, those late modals will still be on top of our modals, because Native components have the highest priorities.
How to Integrate the Library
Step 1: Add the library to your project:
yarn add @whitespectre/rn-modal-presenter
npm install @whitespectre/rn-modal-presenter
Step 2: Wrap the component on top of which you want to present the modals:
import { ModalPresenterParent, showModal } from '@whitespectre/rn-modal-presenter';
…
<ModalPresenterParent>
<App />
</ModalPresenterParent>
Step 3: Call the showModal method which receives:
- The component that will be shown
- The props to be sent to that component
- @returns a ModalHandler that you’ll use to dismiss the modal later
export declare const showModal: <ContentProps>(
Content: (props: ContentProps & ModalContentProps) => JSX.Element,
contentProps: ContentProps,
) => ModalHandler;
And that will be it. You are ready to go.
More Complex Implementations
Passing properties to your component
If you want to add properties to the component you want to present on top of the modal, you can create a helper function on your component.
In this case, you can create the helper function, that is going to call the showModal function, but it receives the properties that the custom text modal receives, which is a text to show, and then a completion handler to execute when the user presses the close button.
export const showCustomAlert = (
title: string,
body: string,
buttons: CustomAlertButton[] = [defaultButton],
) => {
return showModal(CustomAlert, { title, body, buttons });
};
Adding the ModalContentProps to your component properties
This functionality is provided by the library when installing the component and includes the dismiss function.
const CustomAlert = ({
dismiss,
title,
body,
buttons
}: CustomAlertProps & ModalContentProps) => {
return (
…
Here we are passing the dismiss function, which is returned by the modal component properties, and we use that dismiss to clear the model from within the model itself.
Our original example with the new library
Because the rn-modal-presenter library doesn’t require us to manage states and we can just trigger the modal presentations imperatively from anywhere, our original example can be re-written in the following way.
The main App component will just present the Gadget Activation modal:
const App = () => {
return (
<ModalPresenterParent>
<SafeAreaView>
<Button
title="Activate Gadget"
onPress={() => {
showModal(ActivateGadgetModal, {});
}}
/>
</SafeAreaView>
</ModalPresenterParent>
);
};
And then each modal will be responsible for dismissing itself, and triggering the presentation of the next modal in the flow. For example the ActivateGadgetModal would look as follows:
const ActivateGadgetModal = ({dismiss}: ModalContentProps) => {
return (
<View style={styles.modalOverlay}>
<View style={styles.modal}>
<View style={styles.contentContainer}>
<Text>Your Gadget has been activated</Text>
<View style={styles.buttonsContainer}>
<Button
title="Close"
onPress={() => {
dismiss();
showModal(RateAppModal, {});
}}
/>
</View>
</View>
</View>
</View>
);
};
Future Improvements
The rn-modal-presenter library was created to fit our needs in our ongoing projects, but as we started using it we also identified other features and improvements that might be useful for other use cases.
Here are the main features and improvements we would like to introduce to our library:
- Create a queue of modals to show one by one
- Right now if multiple modals try to be shown at once, they appear on top of each other
- The queue should include a prioritization mechanism to force a modal to be shown next.
- Make the views appear on top of the native views
- Build a native module to show the content on top of the native views as well.
- Allow customizable animations and durations for each effect
If you want to implement any of these features or if you just want to contribute to our library, feel free to open PRs at: https://github.com/whitespectre/rn-modal-presenter.