This guide explains how to use the global modal system implemented in this project.
The global modal system allows you to trigger a bottom sheet modal from anywhere in your app programmatically, and render any component inside it.
The system consists of three main parts:
- GlobalModalProvider (
providers/GlobalModalProvider.tsx) - Context provider that manages modal state - GlobalModal (
components/GlobalModal.tsx) - The actual modal component rendered at root level - useGlobalModal hook - Hook to interact with the modal from anywhere
The system is already integrated into your app:
// In app/_layout.tsx
<BottomSheetModalProvider>
<GlobalModalProvider>
{/* Your app content */}
<GlobalModal />
</GlobalModalProvider>
</BottomSheetModalProvider>import { useGlobalModal } from "@/providers/GlobalModalProvider";
import { View, Text } from "react-native";
function MyComponent() {
const { showModal, hideModal } = useGlobalModal();
const handleOpenModal = () => {
showModal(
<View className='p-6'>
<Text className='text-white text-2xl'>Hello from Modal!</Text>
</View>
);
};
return (
<Button onPress={handleOpenModal} title="Open Modal" />
);
}const handleOpenModal = () => {
showModal(
<YourCustomComponent />,
{
snapPoints: ["25%", "50%", "90%"], // Custom snap points
enablePanDownToClose: true, // Allow swipe to close
backgroundStyle: { // Custom background
backgroundColor: "#000000",
},
}
);
};// Open modal
showModal(<Content />);
// Close modal from within the modal content
function ModalContent() {
const { hideModal } = useGlobalModal();
return (
<View>
<Button onPress={hideModal} title="Close" />
</View>
);
}
// Close modal from outside
hideModal();function useApiCall() {
const { showModal } = useGlobalModal();
const fetchData = async () => {
try {
const result = await api.fetch();
// Show success modal
showModal(
<SuccessMessage data={result} />
);
} catch (error) {
// Show error modal
showModal(
<ErrorMessage error={error} />
);
}
};
return fetchData;
}Returns an object with the following properties:
-
showModal(content, options?)- Show the modal with given contentcontent: ReactNode- Any React component or element to renderoptions?: ModalOptions- Optional configuration object
-
hideModal()- Programmatically hide the modal -
isVisible: boolean- Current visibility state of the modal
interface ModalOptions {
enableDynamicSizing?: boolean; // Auto-size based on content (default: true)
snapPoints?: (string | number)[]; // Fixed snap points (e.g., ["50%", "90%"])
enablePanDownToClose?: boolean; // Allow swipe down to close (default: true)
backgroundStyle?: object; // Custom background styles
handleIndicatorStyle?: object; // Custom handle indicator styles
}See components/ExampleGlobalModalUsage.tsx for comprehensive examples including:
- Simple content modal
- Modal with custom snap points
- Complex component in modal
- Success/error modals triggered from functions
The modal uses these default styles (can be overridden via options):
{
enableDynamicSizing: true,
enablePanDownToClose: true,
backgroundStyle: {
backgroundColor: "#171717", // Dark background
},
handleIndicatorStyle: {
backgroundColor: "white",
},
}- Keep content in separate components - Don't inline large JSX in
showModal()calls - Use the hook in custom hooks - Create specialized hooks like
useShowSuccessModal()for reusable modal patterns - Handle cleanup - The modal automatically clears content when closed
- Avoid nesting - Don't show modals from within modals
- Consider UX - Only use for important, contextual information that requires user attention
When using PlatformDropdown with option groups, avoid setting a title on the OptionGroup if you're already passing a title prop to PlatformDropdown. This prevents nested menu behavior on iOS where users have to click through an extra layer.
// Good - No title in option group (title is on PlatformDropdown)
const optionGroups: OptionGroup[] = [
{
options: items.map((item) => ({
type: "radio",
label: item.name,
value: item,
selected: item.id === selected?.id,
onPress: () => onChange(item),
})),
},
];
<PlatformDropdown
groups={optionGroups}
title="Select Item" // Title here
// ...
/>
// Bad - Causes nested menu on iOS
const optionGroups: OptionGroup[] = [
{
title: "Items", // This creates a nested Picker on iOS
options: items.map((item) => ({
type: "radio",
label: item.name,
value: item,
selected: item.id === selected?.id,
onPress: () => onChange(item),
})),
},
];- Ensure
GlobalModalProvideris above the component callinguseGlobalModal() - Check that
BottomSheetModalProvideris present in the tree - Verify
GlobalModalcomponent is rendered
- Use
enableDynamicSizing: truefor auto-sizing - Or specify appropriate
snapPoints
- Ensure
enablePanDownToCloseistrue - Check that backdrop is clickable
- Use
hideModal()for programmatic closing