A guide on how to master React Native modal complex flows.
Do you find using modals in React Native to be a bit of a pain? You’re not alone! Trying to keep control of its open state and repeating the code everywhere you want to use it can be pretty tedious.
And the problem only gets worse when you try to create complex flows. Once you get past two modals, your main component is a mess, and the state is all over the place.
I’ve experienced this first-hand in many big companies, like Zé Delivery (by AB-InBev), Alfred Delivery, and now at X-Team.
But don’t despair! Most people think that’s the only way. Still, in this article, I will explain how problems start and how to deal with them elegantly while improving your development experience. It will even help you use modals inside and outside React components (in Sagas, for example!)
This article's teachings have been encapsulated in this library:
https://github.com/GSTJ/react-native-magic-modal.
The Challenges Of A “Simple Flow”
Imagine this. You work on Facebook, and the Product Team asks for a ‘simple flow’:
As a company, I would like to show a modal asking the user to rate the app between 0–5 stars after the user likes a post for the first time.
a. If the user rates it with less than four stars, show another modal asking for feedback on how to improve.
b. Otherwise, show a happy modal asking the user to rate us on the app store.Finally, show a ‘thanks’ modal, thanking the user for their support.
It isn’t so far-fetched, right?
Four modals deep, it brings up a lot of questions from the developer's side. Where should this logic be placed? How should we process the output from the last modal? How do we keep this clean?
Can you spot what the code will be like? Do you see yourself writing complex logic to handle the order of the modals, making sure every modal has already transitioned to not visible and having tons of useStates in place?
The growth of the problem
Imagine you finally did it, you finished the flow, and the Product Team loves the outcome! They now want to expand this flow to show up only once whenever the user likes anything for the first time, whether it’s a post, a comment, or a product. How would you approach it?
You will have to copy-paste all those four modals on every screen where liking is possible. Maybe you even go ahead and turn all of those into a single component. Even then, adding this wrapper component to every screen is still needed.
A few months pass by, and the Development Team now sees the complexity of handling likes differently on every screen and wants to pass this responsibility to a Redux Saga that can be called from anywhere. How do you show the modal only when the Saga’s Action is fired? Sagas run outside React components.
I can confidently say that I’ve experienced those scenarios happening. Working in the food delivery industry, we consistently asked the user to rate the app, the delivery, and their purchase.
How to avoid it?
Let’s start by tackling the state. Managing it is one of the most important things while working with modals.
Expose Internal Properties With “useImperativeHandle”
In short, useImperativeHandle allows you to expose internal properties via Ref. If you have a modal component, you could use useImperativeHandle to expose their show
and hide
functions. Meaning it can take care of its own state without delegating to its parent component with props, for example.
This can be helpful when you want to make your code clearer and avoid passing down many props. Let’s give it a try:
import React, { useState, useImperativeHandle } from 'react';
import { Text } from 'react-native';
import ModalContainer from 'react-native-modal';
export const ExampleModal = React.forwardRef((ref) => {
const [isVisible, setIsVisible] = useState(false);
const show = () => setIsVisible(true);
const hide = () => setIsVisible(false);
useImperativeHandle(ref, () => ({ hide, show }));
return (
<ModalContainer onBackdropPress={hide} isVisible={isVisible}>
<Text>My awesome modal!</Text>
</ModalContainer>
);
});
That's the transition to a better place.
While a step in the right direction, it doesn’t solve all of our problems by itself. Namely:
- To use it, we need to pass a
ref
prop from auseRef
hook, which means we can't callshow
outside React components. - We still need to instantiate the component on every screen. There’s no way to use it on multiple screens without having
ExampleModal
repeated.
Externally exposing the component’s Ref
Most people don't know this, but React has a createRef method that can be used outside React components. In fact, it's even in the React-Navigation documentation for edge cases.
https://reactnavigation.org/docs/navigating-without-navigation-prop/
In practice, the flexibility it brings can be seen here:
import React, { useState, useImperativeHandle } from 'react';
import { Text } from 'react-native';
import ModalContainer from 'react-native-modal';
export const imperativeModalRef = React.createRef();
export const SmartExample = () => {
const [isVisible, setIsVisible] = useState(false);
const show = () => setIsVisible(true);
const hide = () => setIsVisible(false);
useImperativeHandle(imperativeModalRef, () => ({ hide, show }));
return (
<ModalContainer onBackdropPress={hide} isVisible={isVisible}>
<Text>My awesome modal!</Text>
</ModalContainer>
);
};
Now imperativeModalRef
can be imported and used anywhere, as long as SmartExample
is on the root. We can use show and hide from every component, function, or Saga.
That solves most of our issues, but there's still one: It is hard to manage a project with many modal refs and modals on the root.
That's where you can get creative with abstractions to make the SmartExample
render any modal you want! One way to do this is to make the show
function receive a component and render it.
Going the extra mile
Instead of making you guys reinvent the wheel, I've created an open-source library that encapsulates all these concepts and more, with full TypeScript support based on react-native-modal.
Here's a basic idea of how to use it:
"Talk is cheap. Show me the code." ― Linus Torvalds.
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { MagicModalPortal, magicModal } from 'react-native-magic-modal';
const ConfirmationModal = () => (
<View>
<TouchableOpacity onPress={() => magicModal.hide({ success: true })}>
<Text>Click here to confirm</Text>
</TouchableOpacity>
</View>
);
const ResponseModal = ({ text }) => (
<View>
<Text>{text}</Text>
<TouchableOpacity onPress={() => magicModal.hide()}>
<Text>Close</Text>
</TouchableOpacity>
</View>
);
const handleConfirmationFlow = async () => {
// We can call it with or without props, depending on the requirements of the modal.
const result = await magicModal.show(ConfirmationModal);
if (result.success) {
return magicModal.show(() => <ResponseModal text="Success!" />);
}
return magicModal.show(() => <ResponseModal text="Failure :(" />);
};
export const MainScreen = () => {
return (
<View>
<TouchableOpacity onPress={handleConfirmationFlow}>
<Text>Start the modal flow!</Text>
</TouchableOpacity>
<MagicModalPortal />
</View>
);
};
Example using react-native-magic-modal
As you can see, it gives you loads of flexibility, unimaginable before, by simply abstracting the concepts we’ve gone through.
Now, the confirmation flow can be called from anywhere. Inside or outside React components.
Also, it automatically deals with common issues regarding modals.
Did you know that, in React Native, you can’t show two modals simultaneously?
Even if you try to show a modal right after another, it would probably fail as the last modal is still animating its ‘close’ state. Fortunately, the issue is already dealt with on our side.
https://github.com/react-native-modal/react-native-modal/issues/30
The React Native Magic Modal documentation is easy to understand and will give you a headstart. You can learn more about it here:
GSTJ / react-native-magic-modal
🦄 A modal library that can be called imperatively from anywhere!
React Native Magic Modal 🦄
Note
Simplify your modal management in React Native with the React Native Magic Modal library. Effortlessly control modals, streamline complex flows, and create a seamless user experience.
Tip
Our new version just got released with full support for multiple modals! See the breaking changes.
Features
- 📲 Easy Integration: Seamlessly integrate with your React Native app.
- 🔄 Complex Flow Management: Manage intricate modal sequences effortlessly.
- 🔧 Customizable: Tailor modals to fit your app's unique requirements.
Highlights
React Native Magic Modal offers a superior experience compared to traditional modal implementations:
- 🎨 Stylish and Responsive: Designed to look great on both iOS and Android.
- 🚀 Developer Friendly: Simple to use, with a focus on developer experience.
- 🧩 Versatile: Adaptable to a wide range of modal scenarios.
Table of Contents
This same logic has already been battle-tested on big companies I’ve worked on, like Zé Delivery (by AB-InBev), Alfred Delivery, and X-Team.
Contributions accepted!
Thanks for the read. If you liked the article, follow me on Linkedin and Github.
Top comments (0)