Global Modal in React Native

Suson Thapa
The Startup
Published in
5 min readAug 13, 2023

--

Photo by Martin Sanchez on Unsplash

Believe it or not Modal is an integral part of modern application development. Whether you want to show an error message or a success message Modals come in very handy.

However, after working with React Native for a while I can say it doesn’t play nicely with Modals. First, there are a couple of issues on GitHub about React Native not supporting showing multiple Modals. If you do a quick GitHub issues search you will find dozens of issues related to Modals. I personally have faced a lot of issues when showing multiple Modals.

This is especially problematic on iOS. On iOS, if you try to show a modal before the previous modal disappears it might freeze the app, more info here.

What can we do about it?

Well, we can wait for React Native to fix it or use a library like react-native-modal which exposes props that let you know when the Modal has been dismissed completely so you can synchronize the showing of the next Modal.

I still think that when we have a use-case to show multiple Modals, we can still do it better.

I present you the solution, behold the Global Modal. As the name suggests, instead of having Modals spread across components we will just have one Global Modal and it will handle everything.

This is going to be a bit complicated so let’s outline the flow.

So, we want to have a single Modal component, and whenever we want to show/hide the Modal we will interact with that component.

This component will make sure only one Modal is active at a time and also allows us to develop an entire flow based on Modals.

Show me the solution

iOS

Android

How to do it?

Implementing the Logic

Since we only have one Modal we need to implement logic to show multiple content. We will use events to show and hide the content.

When we want to show content we will emit an event that will make the Modal visible if it isn’t already. If we want to show another Modal then we will emit another event. This will cause the new content to be shown on top of the old content within the same Modal instead of creating a new one.

If we want to hide the content, we will emit another event that will remove the top content and will hide the Modal if there is no content to show.

To implement this logic we will maintain an array where we can push the contents that we want to show.

This is how we will modal the content.

export type GlobalModalProps = {
skipQueue?: boolean;
modalKey?: string,
Component: React.FC
};

The skipQueue attribute will not add the content to the array. The modalKey is used to uniquely identify the content so that we can dismiss it later. The Component attribute is where you specify the content that you want to render.

We will have two events SHOW_GLOBAL_MODAL and HIDE_GLOBAL_MODAL . We listen to SHOW_GLOBAL_MODAL and HIDE_GLOBAL_MODAL and manipulate the content array accordingly.

  useEffect(() => {
const showSub = DeviceEventEmitter.addListener(
SHOW_GLOBAL_MODAL,
(prop: GlobalModalProps) => {
setModalProps((oldProps) => {
return [
// remove any previous content that had skipQueue set to true
...oldProps.filter((it) => !it.skipQueue),
// add a default key if necessary
{ ...prop, modalKey: prop.modalKey ?? Date.now().toString() },
]
});
}
);
const hideSub = DeviceEventEmitter.addListener(HIDE_GLOBAL_MODAL, (key: string) => {
setModalProps((oldProps) => {
return oldProps.filter((it) => it.modalKey !== key)
})
})
return () => {
showSub.remove();
hideSub.remove()
};
}, []);

The code is pretty straightforward. When we receive an event to show the content we just add it to the array. We will filter any previous content that had the attribute skipQueue set to true.

For removing the content, we just remove it.

Implementing the Animation

The idea is pretty simple but making it look nice was a challenge. I am using the Reanimated library to implement the animation. I’m especially using the LayoutAnimations to implement a nice size change animation when the content changes.

This is a very basic code to get the layout animation going.

    <Modal
animationType='none'
transparent
visible={modalVisible}
onRequestClose={closeModal}
>
<Animated.View style={[styles.backdrop, backdropOpacityStyle]}></Animated.View>
<Animated.View style={[styles.centeredView, containerOpacityStyle]}>
<Animated.View style={styles.modalView} layout={Layout.delay(CHILD_ANIM_DURATION).duration(LAYOUT_ANIM_DURATION)}>
{modalProps.map((it, index) => (
<ChildWrapper key={it.modalKey} isEnabled={index === modalProps.length - 1}>
<it.Component />
</ChildWrapper>
))}
</Animated.View>
</Animated.View>
</Modal>

We go through the array of content and render it making sure only the top one is enabled. The ChildWrapper component handles the rendering of the content. If the GlobalModal gets a request to show multiple content, then it will show it on top of another and if the user dismisses the current content then the previous content will show(unless skipQueue has been set).

Using the Modal

We can expose functions to make it easy to show and hide the Modal like this.

export function showGlobalModal(prop: GlobalModalProps) {
DeviceEventEmitter.emit(SHOW_GLOBAL_MODAL, prop);
}

export function hideGlobalModal(key: string) {
DeviceEventEmitter.emit(HIDE_GLOBAL_MODAL, key)
}

Let’s see how we can use this to show an Alert to the user. Let’s define the component first.

type Props = {
onCancelPress: () => void,
onYesPress: () => void,
}

const Confirmation = (props: Props) => {
return (
<View>
<Text style={{
fontSize: 24,
fontWeight: 'bold',
color: 'gray',
}}>Are you sure?</Text>
<View style={{
flexDirection: 'row',
marginTop: 16,
}}>
<TouchableOpacity style={{
flex: 1,
backgroundColor: '#FF5252',
...styles.button
}} onPress={props.onCancelPress}>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={{
flex: 1,
backgroundColor: '#7C4DFF',
...styles.button,
}} onPress={props.onYesPress}>
<Text style={styles.buttonText}>Yes</Text>
</TouchableOpacity>
</View>
</View>
)
}

Now we can use the functions defined earlier to show/hide the Modal like this.

showGlobalModal({
modalKey: 'confirmation-modal',
Component: () => (
<Confirmation
onCancelPress={() => hideGlobalModal('confirmation-modal')}
onYesPress={() => hideGlobalModal('confirmation-modal')} />
),
hideClose: true
})

The actual code is a bit involved but I think you got the gist of it. I had to resort to needsOffscreenAlphaCompositing on Android as I was running into opacity issues when implementing the fade animation.

Here is the Github repository if you want to check out the code.

And that’s it for the story. Catch you on the next one, peace!

--

--

Suson Thapa
The Startup

Android | iOS | Flutter | ReactNative — Passionate Software Engineer