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](https://(https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers) 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 this is a step in the right direction, it doesn't solve all of our problems. 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.
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
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
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 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.
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.
The
Contributions accepted!
Thanks for the read. If you liked the article, follow me on Linkedin and Github.
Also Published here