[ BUG ] Portal issue on iOS, RN without expo
Describe the bug
It seems to be portal related, I currently working with AlertDialog but every component that requires PortalHost has the same issue.
React Native without expo.
Working on iOS, didn't tried Android yet.
While pressing the trigger, the state actually changes, open property goes from false to true, but nothing happens on screen.
I've tested directly using Portal and it seems to work as aspected.
I have <PortalHost/> as last child in my main app.tsx file.
function App() {
return (
<>
<GestureHandlerRootView>
<SafeAreaProvider>
<LocalizationProvider>
<NetworkStatusProvider>
<QueryProvider>
<AuthProvider>
<ClarityProvider>
<NavigationProvider />
</ClarityProvider>
</AuthProvider>
</QueryProvider>
</NetworkStatusProvider>
</LocalizationProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
<PortalHost />
</>
);
}
Testing screen with AlertDialog and direct portal test
import React, {useState} from 'react';
import {SafeAreaView, View} from 'react-native';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '~/components/ui/alert-dialog';
import {Button} from '~/components/ui/button';
import {Portal} from '@rn-primitives/portal';
import {Text} from '~/components/ui/text/text.component';
export default function TestScreen() {
const [showPortalTest, setShowPortalTest] = useState(false);
// Test portal directly
const togglePortalTest = () => {
setShowPortalTest(!showPortalTest);
};
return (
<SafeAreaView
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 20,
}}>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Text>Show Alert Dialog</Text>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Text>Cancel</Text>
</AlertDialogCancel>
<AlertDialogAction>
<Text>Continue</Text>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Direct portal test */}
<Button onPress={togglePortalTest}>
<Text>Toggle Direct Portal Test</Text>
</Button>
{/* Direct portal test */}
{showPortalTest && (
<Portal name="test-portal">
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}>
<View
style={{
backgroundColor: 'white',
padding: 20,
borderRadius: 8,
margin: 20,
}}>
<Text style={{color: 'black', marginBottom: 10}}>
Portal Test Working!
</Text>
<Button onPress={togglePortalTest}>
<Text>Close Portal Test</Text>
</Button>
</View>
</View>
</Portal>
)}
</SafeAreaView>
);
}
Pressing on "Show Alert Dialog" button, show nothing, but the state get updated. Pressing on "Toggle Direct Portal Test" actually shows a Dialog, so I assume that the PortalHost configuration is fine.
Expected behavior Dialog should appears.
Screenshots If applicable, add screenshots to help explain your problem.
Platform (please complete the following information):
- Type: Simulator, Device
- OS: iOS
Hey @ndrkltsk, can you provide a minimal reproduction repo?
same issues but getting this error:
ERROR Warning: Error: NativeViewGestureHandler must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/installation for more details.
main layout file: layout.tsx
import "~/global.css";
import {
DarkTheme,
DefaultTheme,
Theme,
ThemeProvider,
} from "@react-navigation/native";
import { setAndroidNavigationBar } from "~/lib/android-navigation-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { QueryProvider } from "~/providers/query-provider";
import { useColorScheme } from "~/hooks/use-color-scheme";
import { AuthProvider } from "~/contexts/auth-context";
import { Appearance, Platform } from "react-native";
import { PortalHost } from "@rn-primitives/portal";
import { StatusBar } from "expo-status-bar";
import { NAV_THEME } from "~/lib/constants";
import { Stack } from "expo-router";
import * as React from "react";
const LIGHT_THEME: Theme = {
...DefaultTheme,
colors: NAV_THEME.light,
};
const DARK_THEME: Theme = {
...DarkTheme,
colors: NAV_THEME.dark,
};
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from "expo-router";
const usePlatformSpecificSetup = Platform.select({
web: useSetWebBackgroundClassName,
android: useSetAndroidNavigationBar,
default: noop,
});
export default function RootLayout() {
usePlatformSpecificSetup();
const { isDarkColorScheme, themeColors } = useColorScheme();
return (
<QueryProvider>
<AuthProvider>
<ThemeProvider
value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}
>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(app)" />
<Stack.Screen name="(auth)" />
</Stack>
</GestureHandlerRootView>
<PortalHost />
</ThemeProvider>
</AuthProvider>
</QueryProvider>
);
}
const useIsomorphicLayoutEffect =
Platform.OS === "web" && typeof window === "undefined"
? React.useEffect
: React.useLayoutEffect;
function useSetWebBackgroundClassName() {
useIsomorphicLayoutEffect(() => {
// Adds the background color to the html element to prevent white background on overscroll.
document.documentElement.classList.add("bg-background");
}, []);
}
function useSetAndroidNavigationBar() {
React.useLayoutEffect(() => {
setAndroidNavigationBar(Appearance.getColorScheme() ?? "light");
}, []);
}
function noop() { }
It worked when I put the PortHost inside the GestureHandlerRootView, but about 1 out of 3 times, the tooltip crashes the entire app in both development and production.
@mohitramani249 Yes, if you put the PortalHost outside of a provider, the content inside the Portal components like the Tooltip will no longer have access to the content of that provider.
Not sure I understand this part:
some 1 of 3 times tooltip crash the whole app in all build
Does that mean the app crashes when the PortalHost is outside of the Provider?
@mohitramani249 Yes, if you put the PortalHost outside of a provider, the content inside the Portal components like the Tooltip will no longer have access to the content of that provider.
Not sure I understand this part:
some 1 of 3 times tooltip crash the whole app in all build
Does that mean the app crashes when the PortalHost is outside of the Provider?
It worked when I put the PortHost inside the GestureHandlerRootView, but about 1 out of 3 times, the tooltip crashes the entire app in both development and production.
For this also, It also crashes sometimes, randomly.
import "~/global.css";
import {
DarkTheme,
DefaultTheme,
Theme,
ThemeProvider,
} from "@react-navigation/native";
import { setAndroidNavigationBar } from "~/lib/android-navigation-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { QueryProvider } from "~/providers/query-provider";
import { useColorScheme } from "~/hooks/use-color-scheme";
import { AuthProvider } from "~/contexts/auth-context";
import { Appearance, Platform } from "react-native";
import { PortalHost } from "@rn-primitives/portal";
import { StatusBar } from "expo-status-bar";
import { NAV_THEME } from "~/lib/constants";
import { Stack } from "expo-router";
import * as React from "react";
const LIGHT_THEME: Theme = {
...DefaultTheme,
colors: NAV_THEME.light,
};
const DARK_THEME: Theme = {
...DarkTheme,
colors: NAV_THEME.dark,
};
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from "expo-router";
const usePlatformSpecificSetup = Platform.select({
web: useSetWebBackgroundClassName,
android: useSetAndroidNavigationBar,
default: noop,
});
// Placeholder providers - implement these based on your needs
const LocalizationProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
const NetworkStatusProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
const ClarityProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
const NavigationProvider = () => {
const { isDarkColorScheme } = useColorScheme();
return (
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(app)" />
<Stack.Screen name="(auth)" />
</Stack>
<PortalHost />
</ThemeProvider>
);
};
export default function RootLayout() {
usePlatformSpecificSetup();
return (
<>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<LocalizationProvider>
<NetworkStatusProvider>
<QueryProvider>
<AuthProvider>
<ClarityProvider>
<NavigationProvider />
</ClarityProvider>
</AuthProvider>
</QueryProvider>
</NetworkStatusProvider>
</LocalizationProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
</>
);
}
const useIsomorphicLayoutEffect =
Platform.OS === "web" && typeof window === "undefined"
? React.useEffect
: React.useLayoutEffect;
function useSetWebBackgroundClassName() {
useIsomorphicLayoutEffect(() => {
// Adds the background color to the html element to prevent white background on overscroll.
document.documentElement.classList.add("bg-background");
}, []);
}
function useSetAndroidNavigationBar() {
React.useLayoutEffect(() => {
setAndroidNavigationBar(Appearance.getColorScheme() ?? "light");
}, []);
}
function noop() { }
@mohitramani249 please create a new issue with a minimal reproduction repo (required)
@mrzachnugent Thanks for the support! I’ll do this ASAP. Here’s a screenshot of the error I’m getting:
@mohitramani249 please create a new issue with a minimal reproduction repo (required)
FIX:
I had this issue running in a PNPM monorepo, without Expo, on RN for Windows, Android, and iOS. While I had placed PortalHost from @rn-primitives/portal in my App.tsx as the docs instruct, and @rn-primitives/select uses the same @rn-primitives/portal import internally, these imports did not use the same instance. This means that, even though I only have one @rn-primitives/portal in my node_modules folder, the context and Zustand store in @rn-primitives/portal used different instances between my app and @rn-primitives/select. I have patched the @rn-primitives/select package to re-export its instance of PortalHost, and am using this in my App.tsx.
import React from "react";
import "./global.css";
import { PortalHost } from "@rn-primitives/portal";
import { PortalHost as SelectPortalHost } from "@rn-primitives/select";
export default function App() {
return (
<>
{/* Place other components here */}
<PortalHost />
<SelectPortalHost />
</>
);
}
diff --git a/dist/select.d.mts b/dist/select.d.mts
index 3292308c0c1f93a4f784955afdaaf4179889f6ab..d1801a69e335a54860798f42008b3b63b207445f 100644
--- a/dist/select.d.mts
+++ b/dist/select.d.mts
@@ -3,6 +3,7 @@ import { ItemTextProps, PortalProps, Option, ScrollDownButtonProps, ScrollUpButt
import * as react_native from 'react-native';
import { View, Text, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
+import { PortalHost } from "@rn-primitives/portal";
import * as React from 'react';
import './select';
@@ -92,4 +93,4 @@ declare const ScrollUpButton: ({ children }: ScrollUpButtonProps) => React.JSX.E
declare const ScrollDownButton: ({ children }: ScrollDownButtonProps) => React.JSX.Element;
declare const Viewport: ({ children }: ViewportProps) => React.JSX.Element;
-export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
+export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, PortalHost, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
diff --git a/dist/select.d.ts b/dist/select.d.ts
index 4e857ddd6ddd211bca55be90bc198d9b7783fb46..48d5bf66465693b28f6c7f9106333798a0697065 100644
--- a/dist/select.d.ts
+++ b/dist/select.d.ts
@@ -3,6 +3,7 @@ import { ItemTextProps, PortalProps, Option, ScrollDownButtonProps, ScrollUpButt
import * as react_native from 'react-native';
import { View, Text, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
+import { PortalHost } from "@rn-primitives/portal";
import * as React from 'react';
import './select';
@@ -92,4 +93,4 @@ declare const ScrollUpButton: ({ children }: ScrollUpButtonProps) => React.JSX.E
declare const ScrollDownButton: ({ children }: ScrollDownButtonProps) => React.JSX.Element;
declare const Viewport: ({ children }: ViewportProps) => React.JSX.Element;
-export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
+export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, PortalHost, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
diff --git a/dist/select.js b/dist/select.js
index 2c68c403912caca100a9b3baa62a9442002dbe84..24a30b8932b3928bea736724c26e244577433d84 100644
--- a/dist/select.js
+++ b/dist/select.js
@@ -39,6 +39,7 @@ __export(select_exports, {
Label: () => Label,
Overlay: () => Overlay,
Portal: () => Portal,
+ PortalHost: () => import_portal.PortalHost,
Root: () => Root,
ScrollDownButton: () => ScrollDownButton,
ScrollUpButton: () => ScrollUpButton,
@@ -368,6 +369,7 @@ function onStartShouldSetResponder() {
Label,
Overlay,
Portal,
+ PortalHost: import_portal.PortalHost,
Root,
ScrollDownButton,
ScrollUpButton,
diff --git a/dist/select.mjs b/dist/select.mjs
index 5756165f6d17d4949843a5d86c9f4bfda801ede1..de202b4fb4ceabe3eaec296bd483323d15982173 100644
--- a/dist/select.mjs
+++ b/dist/select.mjs
@@ -6,7 +6,7 @@ import {
useControllableState,
useRelativePosition
} from "@rn-primitives/hooks";
-import { Portal as RNPPortal } from "@rn-primitives/portal";
+import { Portal as RNPPortal, PortalHost } from "@rn-primitives/portal";
import * as Slot from "@rn-primitives/slot";
import * as React from "react";
import {
@@ -327,6 +327,7 @@ export {
Label,
Overlay,
Portal,
+ PortalHost,
Root,
ScrollDownButton,
ScrollUpButton,
@mrzachnugent Even the initial app created by npx @react-native-reusables/cli@latest init crashes if you rapidly click the 'i' icon 5 to 10 times. I’ve attached a video of the issue below, and when it crashes, it opens these two weird files in the code editor.
https://github.com/user-attachments/assets/1e172c48-ce79-4ad9-8662-e1038df5a648
I am using version 0.81.1 without Expo, and I am experiencing the same issue
✅ Complete Solution: 4 Patches + Configuration
This solution creates dedicated PortalHost instances for each component type, ensuring proper isolation and functionality.
1. Create Patches
Create these 4 patch files in your patches/ directory:
@rn-primitives+dialog+1.2.0.patch
diff --git a/node_modules/@rn-primitives/dialog/dist/dialog.d.ts b/node_modules/@rn-primitives/dialog/dist/dialog.d.ts
index 3d381e9..46e36a6 100644
--- a/node_modules/@rn-primitives/dialog/dist/dialog.d.ts
+++ b/node_modules/@rn-primitives/dialog/dist/dialog.d.ts
@@ -3,6 +3,7 @@ import * as react_native from 'react-native';
import { View, Text } from 'react-native';
import * as React from 'react';
import { PortalProps, RootContext } from './index.js';
+import { PortalHost } from "@rn-primitives/portal";
import './dialog';
declare const Root: React.ForwardRefExoticComponent<react_native.ViewProps & {
@@ -55,4 +56,4 @@ declare const Description: React.ForwardRefExoticComponent<react_native.TextProp
asChild?: boolean;
} & React.RefAttributes<Text>>;
-export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext };
+export { Close, Content, Description, Overlay, Portal, PortalHost, Root, Title, Trigger, useRootContext };
diff --git a/node_modules/@rn-primitives/dialog/dist/dialog.js b/node_modules/@rn-primitives/dialog/dist/dialog.js
index 4851de2..6a81722 100644
--- a/node_modules/@rn-primitives/dialog/dist/dialog.js
+++ b/node_modules/@rn-primitives/dialog/dist/dialog.js
@@ -36,6 +36,7 @@ __export(dialog_exports, {
Description: () => Description,
Overlay: () => Overlay,
Portal: () => Portal,
+ PortalHost: () => import_portal.PortalHost,
Root: () => Root,
Title: () => Title,
Trigger: () => Trigger,
@@ -198,6 +199,7 @@ function onStartShouldSetResponder() {
Description,
Overlay,
Portal,
+ PortalHost: import_portal.PortalHost,
Root,
Title,
Trigger,
diff --git a/node_modules/@rn-primitives/dialog/dist/dialog.mjs b/node_modules/@rn-primitives/dialog/dist/dialog.mjs
index 9108d09..866ab9b 100644
--- a/node_modules/@rn-primitives/dialog/dist/dialog.mjs
+++ b/node_modules/@rn-primitives/dialog/dist/dialog.mjs
@@ -2,7 +2,7 @@
// src/dialog.tsx
import { useControllableState } from "@rn-primitives/hooks";
-import { Portal as RNPPortal } from "@rn-primitives/portal";
+import { Portal as RNPPortal, PortalHost } from "@rn-primitives/portal";
import * as Slot from "@rn-primitives/slot";
import * as React from "react";
import { BackHandler, Pressable as Pressable2, Text, View as View2 } from "react-native";
@@ -156,6 +156,7 @@ export {
Description,
Overlay,
Portal,
+ PortalHost,
Root,
Title,
Trigger,
@rn-primitives+select+1.2.0.patch
diff --git a/node_modules/@rn-primitives/select/dist/select.d.mts b/node_modules/@rn-primitives/select/dist/select.d.mts
index 3292308..d1801a6 100644
--- a/node_modules/@rn-primitives/select/dist/select.d.mts
+++ b/node_modules/@rn-primitives/select/dist/select.d.mts
@@ -3,6 +3,7 @@ import { ItemTextProps, PortalProps, Option, ScrollDownButtonProps, ScrollUpButt
import * as react_native from 'react-native';
import { View, Text, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
+import { PortalHost } from "@rn-primitives/portal";
import * as React from 'react';
import './select';
@@ -92,4 +93,4 @@ declare const ScrollUpButton: ({ children }: ScrollUpButtonProps) => React.JSX.E
declare const ScrollDownButton: ({ children }: ScrollDownButtonProps) => React.JSX.Element;
declare const Viewport: ({ children }: ViewportProps) => React.JSX.Element;
-export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
+export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, PortalHost, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
diff --git a/node_modules/@rn-primitives/select/dist/select.d.ts b/node_modules/@rn-primitives/select/dist/select.d.ts
index 4e857dd..48d5bf6 100644
--- a/node_modules/@rn-primitives/select/dist/select.d.ts
+++ b/node_modules/@rn-primitives/select/dist/select.d.ts
@@ -3,6 +3,7 @@ import { ItemTextProps, PortalProps, Option, ScrollDownButtonProps, ScrollUpButt
import * as react_native from 'react-native';
import { View, Text, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
+import { PortalHost } from "@rn-primitives/portal";
import * as React from 'react';
import './select';
@@ -92,4 +93,4 @@ declare const ScrollUpButton: ({ children }: ScrollUpButtonProps) => React.JSX.E
declare const ScrollDownButton: ({ children }: ScrollDownButtonProps) => React.JSX.Element;
declare const Viewport: ({ children }: ViewportProps) => React.JSX.Element;
-export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
+export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, PortalHost, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, Value, Viewport, useItemContext, useRootContext };
diff --git a/node_modules/@rn-primitives/select/dist/select.js b/node_modules/@rn-primitives/select/dist/select.js
index 2c68c40..24a30b8 100644
--- a/node_modules/@rn-primitives/select/dist/select.js
+++ b/node_modules/@rn-primitives/select/dist/select.js
@@ -39,6 +39,7 @@ __export(select_exports, {
Label: () => Label,
Overlay: () => Overlay,
Portal: () => Portal,
+ PortalHost: () => import_portal.PortalHost,
Root: () => Root,
ScrollDownButton: () => ScrollDownButton,
ScrollUpButton: () => ScrollUpButton,
@@ -368,6 +369,7 @@ function onStartShouldSetResponder() {
Label,
Overlay,
Portal,
+ PortalHost: import_portal.PortalHost,
Root,
ScrollDownButton,
ScrollUpButton,
diff --git a/node_modules/@rn-primitives/select/dist/select.mjs b/node_modules/@rn-primitives/select/dist/select.mjs
index 5756165..de202b4 100644
--- a/node_modules/@rn-primitives/select/dist/select.mjs
+++ b/node_modules/@rn-primitives/select/dist/select.mjs
@@ -6,7 +6,7 @@ import {
useControllableState,
useRelativePosition
} from "@rn-primitives/hooks";
-import { Portal as RNPPortal } from "@rn-primitives/portal";
+import { Portal as RNPPortal, PortalHost } from "@rn-primitives/portal";
import * as Slot from "@rn-primitives/slot";
import * as React from "react";
import {
@@ -327,6 +327,7 @@ export {
Label,
Overlay,
Portal,
+ PortalHost,
Root,
ScrollDownButton,
ScrollUpButton,
@rn-primitives+dropdown-menu+1.2.0.patch
diff --git a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.d.ts b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.d.ts
index aee9743..b5b65e4 100644
--- a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.d.ts
+++ b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.d.ts
@@ -4,6 +4,7 @@ import { View, Text, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
import * as React from 'react';
import { PortalProps, TriggerRef } from './index.js';
+import { PortalHost } from "@rn-primitives/portal";
import './dropdown-menu';
interface IRootContext {
@@ -125,4 +126,4 @@ declare const SubContent: React.ForwardRefExoticComponent<Omit<react_native.Pres
onKeyUp?: (ev: React.KeyboardEvent) => void;
} & _rn_primitives_types.ForceMountable & React.RefAttributes<View>>;
-export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useRootContext, useSubContext };
+export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Overlay, Portal, PortalHost, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useRootContext, useSubContext };
diff --git a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.js b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.js
index 1560b2f..194eb4f 100644
--- a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.js
+++ b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.js
@@ -39,6 +39,7 @@ __export(dropdown_menu_exports, {
Label: () => Label,
Overlay: () => Overlay,
Portal: () => Portal,
+ PortalHost: () => import_portal.PortalHost,
RadioGroup: () => RadioGroup,
RadioItem: () => RadioItem,
Root: () => Root,
@@ -472,6 +473,7 @@ Content.displayName = "ContentNativeDropdownMenu";
Label,
Overlay,
Portal,
+ PortalHost: import_portal.PortalHost,
RadioGroup,
RadioItem,
Root,
diff --git a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.mjs b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.mjs
index 9e28da3..f7b6f04 100644
--- a/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.mjs
+++ b/node_modules/@rn-primitives/dropdown-menu/dist/dropdown-menu.mjs
@@ -6,7 +6,7 @@ import {
useControllableState,
useRelativePosition
} from "@rn-primitives/hooks";
-import { Portal as RNPPortal } from "@rn-primitives/portal";
+import { Portal as RNPPortal, PortalHost } from "@rn-primitives/portal";
import * as Slot from "@rn-primitives/slot";
import * as React from "react";
import {
@@ -430,6 +430,7 @@ export {
Label,
Overlay,
Portal,
+ PortalHost,
RadioGroup,
RadioItem,
Root,
@rn-primitives+popover+1.2.0.patch
diff --git a/node_modules/@rn-primitives/popover/dist/popover.d.ts b/node_modules/@rn-primitives/popover/dist/popover.d.ts
index c02f456..e3ad5c3 100644
--- a/node_modules/@rn-primitives/popover/dist/popover.d.ts
+++ b/node_modules/@rn-primitives/popover/dist/popover.d.ts
@@ -4,6 +4,7 @@ import { View, LayoutRectangle } from 'react-native';
import { LayoutPosition } from '@rn-primitives/hooks';
import * as React from 'react';
import { PortalProps, TriggerRef } from './index.js';
+import { PortalHost } from "@rn-primitives/portal";
import './popover';
interface IRootContext {
@@ -54,4 +55,4 @@ declare const Close: React.ForwardRefExoticComponent<Omit<react_native.Pressable
onKeyUp?: (ev: React.KeyboardEvent) => void;
} & React.RefAttributes<View>>;
-export { Close, Content, Overlay, Portal, Root, Trigger, useRootContext };
+export { Close, Content, Overlay, Portal, PortalHost, Root, Trigger, useRootContext };
diff --git a/node_modules/@rn-primitives/popover/dist/popover.js b/node_modules/@rn-primitives/popover/dist/popover.js
index 04eeab6..1063226 100644
--- a/node_modules/@rn-primitives/popover/dist/popover.js
+++ b/node_modules/@rn-primitives/popover/dist/popover.js
@@ -35,6 +35,7 @@ __export(popover_exports, {
Content: () => Content,
Overlay: () => Overlay,
Portal: () => Portal,
+ PortalHost: () => import_portal.PortalHost,
Root: () => Root,
Trigger: () => Trigger,
useRootContext: () => useRootContext
@@ -255,6 +256,7 @@ function onStartShouldSetResponder() {
Content,
Overlay,
Portal,
+ PortalHost: import_portal.PortalHost,
Root,
Trigger,
useRootContext
diff --git a/node_modules/@rn-primitives/popover/dist/popover.mjs b/node_modules/@rn-primitives/popover/dist/popover.mjs
index eabdc04..25529bd 100644
--- a/node_modules/@rn-primitives/popover/dist/popover.mjs
+++ b/node_modules/@rn-primitives/popover/dist/popover.mjs
@@ -2,7 +2,7 @@
// src/popover.tsx
import { useAugmentedRef, useRelativePosition } from "@rn-primitives/hooks";
-import { Portal as RNPPortal } from "@rn-primitives/portal";
+import { Portal as RNPPortal, PortalHost } from "@rn-primitives/portal";
import * as Slot from "@rn-primitives/slot";
import * as React from "react";
import {
@@ -219,6 +219,7 @@ export {
Content,
Overlay,
Portal,
+ PortalHost,
Root,
Trigger,
useRootContext
2. Configure App.tsx
Update your main App component to include all PortalHosts:
import { PortalHost } from '@rn-primitives/portal';
import { PortalHost as DialogPortalHost } from '@rn-primitives/dialog';
import { PortalHost as SelectPortalHost } from '@rn-primitives/select';
import { PortalHost as PopoverPortalHost } from '@rn-primitives/popover';
import { PortalHost as DropdownMenuPortalHost } from '@rn-primitives/dropdown-menu';
function App() {
return (
<SafeAreaProvider>
<AuthProvider>
<AppNavigator />
<PortalHost />
<DialogPortalHost name="dialog-portal" />
<SelectPortalHost name="select-portal" />
<PopoverPortalHost name="popover-portal" />
<DropdownMenuPortalHost name="dropdown-portal" />
</AuthProvider>
</SafeAreaProvider>
);
}
3. Update UI Components (Optional)
For better isolation, update your UI components to use specific portal names:
// Dialog Component
<DialogPortal hostName={portalHost || 'dialog-portal'}>
// Select Component
<SelectPrimitive.Portal hostName={portalHost || 'select-portal'}>
// Popover Component
<PopoverPrimitive.Portal hostName={portalHost || 'popover-portal'}>
// DropdownMenu Component
<DropdownMenuPrimitive.Portal hostName={portalHost || 'dropdown-portal'}>
4. Apply Patches
Ensure you have patch-package in your package.json:
{
"scripts": {
"postinstall": "patch-package"
},
"devDependencies": {
"patch-package": "^8.0.0"
}
}
How It Works
- Dedicated PortalHosts: Each component type gets its own PortalHost instance
- Isolated Stores: Prevents Zustand store conflicts between different Portal components
- Proper Communication: Each Portal component communicates with its dedicated PortalHost
- Backward Compatibility: Maintains support for custom portalHost props
Benefits
- ✅ Complete Solution: Works for ALL Portal components (Dialog, Select, Popover, DropdownMenu)
- ✅ iOS Device Compatible: Fixes the issue on real iOS devices
- ✅ Scalable: Easy to add more Portal components in the future
- ✅ Robust: Component isolation prevents interference between different Portal types
- ✅ Maintainable: Clean separation of concerns
This solution provides complete coverage for the Portal issue on iOS devices with React Native without Expo.
SelectPrimitive.Portal
Do you have a working example by any chance? I tried all your steps but without any luck.
Component:
const fruits = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Blueberry', value: 'blueberry' },
{ label: 'Grapes', value: 'grapes' },
{ label: 'Pineapple', value: 'pineapple' },
];
const FruitSelector = () => {
const ref = useRef<TriggerRef>(null);
const insets = useSafeAreaInsets();
const contentInsets = {
top: insets.top,
bottom: Platform.select({ ios: insets.bottom, android: insets.bottom + 24 }),
left: 12,
right: 12,
};
// Workaround for rn-primitives/select not opening on mobile
const onTouchStart = () => {
ref.current?.open();
};
return (
<>
{/* <PortalHost name='example-host' /> */}
{/* <SelectPortalHost name='select-portal' /> */}
<Select className='max-h-lg'>
<SelectTrigger ref={ref} onTouchStart={onTouchStart}>
<SelectValue placeholder='Select a fruit' />
</SelectTrigger>
<SelectContent insets={contentInsets} portalHost='select-portal'>
<NativeSelectScrollView>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
{fruits.map((fruit) => (
<SelectItem key={fruit.value} label={fruit.label} value={fruit.value}>
{fruit.label}
</SelectItem>
))}
</SelectGroup>
</NativeSelectScrollView>
</SelectContent>
</Select>
</>
);
};
App.tsx
<SafeAreaProvider>
<GestureHandlerRootView>
<ReactQueryProvider>
<SafeAreaView className='flex-1'>
<StrictMode>
<Component />
</StrictMode>
<Toaster position='top-center' />
</SafeAreaView>
</ReactQueryProvider>
<SelectPortalHost name='select-portal' />
<PortalHost />
</GestureHandlerRootView>
</SafeAreaProvider>
Select component has
<SelectPrimitive.Portal hostName={portalHost || 'select-portal'}>
I checked my node_modules and it does contain the correct patches. Anything else I could be missing?