Host Organization
- Use descriptive host names that reflect their purpose (
modals
,tooltips
,notifications
) - Organize hosts by UI layer (overlays, dropdowns, floating elements)
- Keep host hierarchy shallow to avoid complexity
Portal provides a powerful solution for rendering React components outside their normal component tree hierarchy. It features host-based targeting, dynamic mounting/unmounting, and seamless integration with React’s component lifecycle.
import { Portal, PortalHost, PortalProvider, usePortal } from 'rnc-theme';
<PortalHost name="modal-host"> <App />
{/* Somewhere deep in your component tree */} <Portal hostName="modal-host"> <Modal> <Text>This renders at the PortalHost location!</Text> </Modal> </Portal></PortalHost>
<PortalHost> <App />
{/* Portal without hostName uses default host */} <Portal> <Tooltip>Default host portal content</Tooltip> </Portal></PortalHost>
<PortalHost name="notifications"> <App />
<Portal name="toast-notification" hostName="notifications"> <Toast message="Success!" /> </Portal></PortalHost>
<View> <PortalHost name="modals"> <PortalHost name="tooltips"> <App />
<Portal hostName="modals"> <Modal /> </Portal>
<Portal hostName="tooltips"> <Tooltip /> </Portal> </PortalHost> </PortalHost></View>
Renders children at a specified PortalHost location.
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Content to render at the portal host |
name | string | auto-generated | Unique identifier for the portal |
hostName | string | 'default' | Target host name where content should render |
Defines a mounting point where Portal content will be rendered.
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Normal component tree content |
name | string | 'default' | Host identifier for targeting portals |
Context provider that manages portal state and mounting logic.
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Component tree to provide portal context to |
hostName | string | 'default' | Host name for this provider instance |
Access portal context methods for programmatic control.
const { mount, unmount, update, hostName } = usePortal();
Method | Parameters | Description |
---|---|---|
mount | name: string, children: ReactNode, targetHost?: string | Mount content to a portal |
unmount | name: string, targetHost?: string | Remove content from a portal |
update | name: string, children: ReactNode, targetHost?: string | Update existing portal content |
const App = () => { const [showModal, setShowModal] = useState(false);
return ( <PortalHost name="modal-root"> <View style={{ flex: 1 }}> <Text>Main App Content</Text>
<Button onPress={() => setShowModal(true)}> <ButtonText>Open Modal</ButtonText> </Button>
{showModal && ( <Portal hostName="modal-root" name="main-modal"> <Modal onClose={() => setShowModal(false)}> <View style={styles.modalContent}> <Text>Modal Content</Text> <Button onPress={() => setShowModal(false)}> <ButtonText>Close</ButtonText> </Button> </View> </Modal> </Portal> )} </View> </PortalHost> );};
const ToastProvider = ({ children }) => { const [toasts, setToasts] = useState([]);
const showToast = (message, type = 'info') => { const id = Date.now().toString(); setToasts(prev => [...prev, { id, message, type }]);
// Auto-remove after 3 seconds setTimeout(() => { setToasts(prev => prev.filter(toast => toast.id !== id)); }, 3000); };
return ( <PortalHost name="toast-container"> <ToastContext.Provider value={{ showToast }}> {children}
{toasts.map(toast => ( <Portal key={toast.id} hostName="toast-container"> <Toast message={toast.message} type={toast.type} onDismiss={() => setToasts(prev => prev.filter(t => t.id !== toast.id)) } /> </Portal> ))} </ToastContext.Provider> </PortalHost> );};
const SomeComponent = () => { const { showToast } = useContext(ToastContext);
return ( <Button onPress={() => showToast('Operation successful!', 'success')}> <ButtonText>Show Toast</ButtonText> </Button> );};
const TooltipExample = () => { const [activeTooltip, setActiveTooltip] = useState(null);
return ( <PortalHost name="tooltip-layer"> <View style={{ padding: 20 }}> <Text>Hover over buttons to see tooltips</Text>
<Button onPressIn={() => setActiveTooltip('button1')} onPressOut={() => setActiveTooltip(null)} > <ButtonText>Button 1</ButtonText> </Button>
<Button onPressIn={() => setActiveTooltip('button2')} onPressOut={() => setActiveTooltip(null)} > <ButtonText>Button 2</ButtonText> </Button>
{activeTooltip === 'button1' && ( <Portal hostName="tooltip-layer" name="tooltip-1"> <Tooltip>This is button 1 tooltip</Tooltip> </Portal> )}
{activeTooltip === 'button2' && ( <Portal hostName="tooltip-layer" name="tooltip-2"> <Tooltip>This is button 2 tooltip</Tooltip> </Portal> )} </View> </PortalHost> );};
const ProgrammaticPortalExample = () => { const { mount, unmount, update } = usePortal(); const [counter, setCounter] = useState(0);
const mountPortal = () => { mount( 'dynamic-portal', <View style={styles.dynamicContent}> <Text>Dynamically mounted content!</Text> </View>, 'dynamic-host' ); };
const updatePortal = () => { const newCounter = counter + 1; setCounter(newCounter);
update( 'dynamic-portal', <View style={styles.dynamicContent}> <Text>Updated content: {newCounter}</Text> </View>, 'dynamic-host' ); };
const unmountPortal = () => { unmount('dynamic-portal', 'dynamic-host'); };
return ( <PortalHost name="dynamic-host"> <View style={{ padding: 20 }}> <VStack spacing="md"> <Button onPress={mountPortal}> <ButtonText>Mount Portal</ButtonText> </Button>
<Button onPress={updatePortal}> <ButtonText>Update Portal</ButtonText> </Button>
<Button onPress={unmountPortal} variant="error"> <ButtonText>Unmount Portal</ButtonText> </Button> </VStack> </View> </PortalHost> );};
const MultiHostApp = () => { return ( <PortalHost name="root"> <View style={{ flex: 1 }}> {/* Header with its own portal host for dropdowns */} <PortalHost name="header-overlays"> <Header /> </PortalHost>
{/* Main content area */} <PortalHost name="content-overlays"> <MainContent /> </PortalHost>
{/* Footer with notifications */} <PortalHost name="notifications"> <Footer /> </PortalHost> </View> </PortalHost> );};
const Header = () => { const [showDropdown, setShowDropdown] = useState(false);
return ( <View> <Button onPress={() => setShowDropdown(!showDropdown)}> <ButtonText>Menu</ButtonText> </Button>
{showDropdown && ( <Portal hostName="header-overlays" name="menu-dropdown"> <Dropdown onClose={() => setShowDropdown(false)}> <MenuItem>Profile</MenuItem> <MenuItem>Settings</MenuItem> <MenuItem>Logout</MenuItem> </Dropdown> </Portal> )} </View> );};
const MainContent = () => { const [showModal, setShowModal] = useState(false);
return ( <View> <Text>Main content here</Text>
<Button onPress={() => setShowModal(true)}> <ButtonText>Open Modal</ButtonText> </Button>
{showModal && ( <Portal hostName="content-overlays" name="main-modal"> <Modal onClose={() => setShowModal(false)}> <Text>Modal content</Text> </Modal> </Portal> )} </View> );};
const AnimatedPortal = ({ children, visible, hostName }) => { const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => { Animated.timing(fadeAnim, { toValue: visible ? 1 : 0, duration: 300, useNativeDriver: true, }).start(); }, [visible, fadeAnim]);
if (!visible) return null;
return ( <Portal hostName={hostName}> <Animated.View style={{ opacity: fadeAnim }}> {children} </Animated.View> </Portal> );};
const usePortalManager = (hostName = 'default') => { const { mount, unmount, update } = usePortal(); const portalsRef = useRef(new Set());
const createPortal = useCallback((name, content) => { mount(name, content, hostName); portalsRef.current.add(name); }, [mount, hostName]);
const removePortal = useCallback((name) => { unmount(name, hostName); portalsRef.current.delete(name); }, [unmount, hostName]);
const updatePortal = useCallback((name, content) => { update(name, content, hostName); }, [update, hostName]);
const clearAllPortals = useCallback(() => { portalsRef.current.forEach(name => { unmount(name, hostName); }); portalsRef.current.clear(); }, [unmount, hostName]);
useEffect(() => { return () => { clearAllPortals(); }; }, [clearAllPortals]);
return { createPortal, removePortal, updatePortal, clearAllPortals, };};
const ConditionalPortal = ({ condition, children, hostName, name }) => { const portalRef = useRef(null);
useEffect(() => { if (condition && !portalRef.current) { portalRef.current = true; } else if (!condition && portalRef.current) { portalRef.current = false; } }, [condition]);
if (!condition) return null;
return ( <Portal hostName={hostName} name={name}> {children} </Portal> );};
Host Organization
modals
, tooltips
, notifications
)Portal Lifecycle
Performance
React.memo
for portal content that doesn’t change frequentlyconst useModalStack = () => { const [modals, setModals] = useState([]);
const pushModal = (modal) => { setModals(prev => [...prev, { ...modal, id: Date.now() }]); };
const popModal = () => { setModals(prev => prev.slice(0, -1)); };
const clearModals = () => { setModals([]); };
return { modals, pushModal, popModal, clearModals, hasModals: modals.length > 0, };};
const Drawer = ({ isOpen, children, position = 'right' }) => { if (!isOpen) return null;
return ( <Portal hostName="drawer-host" name={`drawer-${position}`}> <View style={[styles.drawer, styles[position]]}> {children} </View> </Portal> );};