Features
- Defaults to a single host
- Supports multiple hosts
- Supports multiple portals for a single host
component. -->
Portals let you render its children into a different part of your app.
Features
Install the component via your command line.
npx expo install @rn-primitives/portal
Install zustand
npx expo install zustand
Copy/paste the following code to ~/components/primitives/portal.tsx
import * as React from 'react';import { Platform, type View, type ViewStyle } from 'react-native';import { create } from 'zustand';
const DEFAULT_PORTAL_HOST = 'INTERNAL_PRIMITIVE_DEFAULT_HOST_NAME';
type PortalMap = Map<string, React.ReactNode>;type PortalHostMap = Map<string, PortalMap>;
const usePortal = create<{ map: PortalHostMap }>(() => ({ map: new Map<string, PortalMap>().set(DEFAULT_PORTAL_HOST, new Map<string, React.ReactNode>()),}));
const updatePortal = (hostName: string, name: string, children: React.ReactNode) => { usePortal.setState((prev) => { const next = new Map(prev.map); const portal = next.get(hostName) ?? new Map<string, React.ReactNode>(); portal.set(name, children); next.set(hostName, portal); return { map: next }; });};const removePortal = (hostName: string, name: string) => { usePortal.setState((prev) => { const next = new Map(prev.map); const portal = next.get(hostName) ?? new Map<string, React.ReactNode>(); portal.delete(name); next.set(hostName, portal); return { map: next }; });};
export function PortalHost({ name = DEFAULT_PORTAL_HOST }: { name?: string }) { const portalMap = usePortal((state) => state.map).get(name) ?? new Map<string, React.ReactNode>(); if (portalMap.size === 0) return null; return <>{Array.from(portalMap.values())}</>;}
export function Portal({ name, hostName = DEFAULT_PORTAL_HOST, children,}: { name: string; hostName?: string; children: React.ReactNode;}) { React.useEffect(() => { updatePortal(hostName, name, children); }, [hostName, name, children]);
React.useEffect(() => { return () => { removePortal(hostName, name); }; }, [hostName, name]);
return null;}
const ROOT: ViewStyle = { flex: 1,};
/** * @deprecated use `FullWindowOverlay` from `react-native-screens` instead * @exampleimport { FullWindowOverlay } from "react-native-screens"const WindowOverlay = Platform.OS === "ios" ? FullWindowOverlay : Fragment// Wrap the `<PortalHost/>` with `<WindowOverlay/>`<WindowOverlay><PortalHost/></WindowOverlay> */export function useModalPortalRoot() { const ref = React.useRef<View>(null); const [sideOffset, setSideOffSet] = React.useState(0);
const onLayout = React.useCallback(() => { if (Platform.OS === 'web') return; ref.current?.measure((_x, _y, _width, _height, _pageX, pageY) => { setSideOffSet(-pageY); }); }, []);
return { ref, sideOffset, onLayout, style: ROOT, };}
Add the <PortalHost />
as the last child of your <Root/>
component (for expo-router, the default export in the root _layout.tsx
)
import { PortalHost } from '@rn-primitives/portal';
function Root() { return ( <> <Stack /> {/* Children of <Portal /> will render here */} <PortalHost /> </> );}
Then, from any component, add a <Portal />
and its content will be rendered as a child of <PortalHost />
import { Portal } from '@rn-primitives/portal';
function Card() { return ( <Wrapper> <Content /> {/* Children of `Portal` will be rendered as a child of `PortalHost` */} {/* It will not render in the `Card` component */} <Portal name='card-portal'> <View style={[ StyleSheet.absoluteFill, { justifyContent: 'center', alignItems: 'center', backgroundColor: 'black', }, ]} > <View> <Text style={{ color: 'white' }}> I am centered and overlay the entier screen </Text> </View> </View> </Portal> </Wrapper> );}
Add the <PortalHost name="unique-host-name"/>
with a unique name where you want the contents of <Portal />
to render. Then, from any component, add a <Portal />
and its content will be rendered as a child of <PortalHost />
import { Portal, PortalHost } from '@rn-primitives/portal';
function Example() { return ( <Wrapper> <PortalHost name='example-host' /> <Content /> <Portal name='example-portal' hostName='example-host'> <View> <Text>I will be rendered above the Content component</Text> </View> </Portal> </Wrapper> );}
By default, children of all Portal components will be rendered as its own children.
Prop | Type | Note |
---|---|---|
name | string | Provide when it is used as a custom host (optional) |
Prop | Type | Note |
---|---|---|
name* | string | Unique value otherwise the portal with the same name will replace the original portal |
hostName | string | Provide when its children are to be rendered in a custom host (optional) |