Skip to content

Portal

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>

Renders children at a specified PortalHost location.

PropTypeDefaultDescription
childrenReact.ReactNode-Content to render at the portal host
namestringauto-generatedUnique identifier for the portal
hostNamestring'default'Target host name where content should render

Defines a mounting point where Portal content will be rendered.

PropTypeDefaultDescription
childrenReact.ReactNode-Normal component tree content
namestring'default'Host identifier for targeting portals

Context provider that manages portal state and mounting logic.

PropTypeDefaultDescription
childrenReact.ReactNode-Component tree to provide portal context to
hostNamestring'default'Host name for this provider instance

Access portal context methods for programmatic control.

const { mount, unmount, update, hostName } = usePortal();
MethodParametersDescription
mountname: string, children: ReactNode, targetHost?: stringMount content to a portal
unmountname: string, targetHost?: stringRemove content from a portal
updatename: string, children: ReactNode, targetHost?: stringUpdate 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

  • 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 Lifecycle

  • Always unmount portals when components unmount to prevent memory leaks
  • Use unique names for portals to avoid conflicts
  • Consider portal cleanup in error boundaries

Performance

  • Minimize portal updates by using stable references
  • Use React.memo for portal content that doesn’t change frequently
  • Consider portal pooling for frequently mounted/unmounted content
const 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>
);
};