Portal
tetra-ui/portalFreeComponent
Renders children into a different part of the app.
Live preview
live · rendered by the registry
Rendered by tetra-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://tetra-ui.com/r/portal.jsonSource
1import { Fragment, useEffect, useSyncExternalStore } from "react";2import { Platform } from "react-native";3import { FullWindowOverlay } from "react-native-screens";45// Constants6const DEFAULT_PORTAL_HOST = "TETRA_UI_DEFAULT_HOST_NAME";78// Types9type PortalHostProps = {10 name?: string;11};1213type PortalProps = {14 name: string;15 hostName?: string;16 children: React.ReactNode;17};1819type PortalMap = Map<string, React.ReactNode>;2021type PortalHostMap = Map<string, PortalMap>;2223type PortalListener = () => void;2425// Components26export const PortalOverlay =27 Platform.OS === "ios" ? FullWindowOverlay : Fragment;2829export const PortalHost = ({ name = DEFAULT_PORTAL_HOST }: PortalHostProps) => {30 const map = usePortalMap();31 const portal = map.get(name) ?? new Map<string, React.ReactNode>();3233 if (portal.size === 0) {34 return null;35 }3637 return <>{Array.from(portal.values())}</>;38};3940export const Portal = ({41 name,42 hostName = DEFAULT_PORTAL_HOST,43 children,44}: PortalProps) => {45 useEffect(() => {46 updatePortal(hostName, name, children);47 }, [hostName, name, children]);4849 useEffect(() => {50 return () => {51 removePortal(hostName, name);52 };53 }, [hostName, name]);5455 return null;56};5758// Hooks59const usePortalMap = () => {60 return useSyncExternalStore(61 (onStoreChange) => {62 listeners.add(onStoreChange);63 return () => {64 listeners.delete(onStoreChange);65 };66 },67 () => portalMap68 );69};7071// Utils72let portalMap: PortalHostMap = new Map<string, PortalMap>().set(73 DEFAULT_PORTAL_HOST,74 new Map<string, React.ReactNode>()75);7677const listeners = new Set<PortalListener>();7879const notifyListeners = () => {80 for (const listener of listeners) {81 listener();82 }83};8485const updatePortal = (86 hostName: string,87 name: string,88 content: React.ReactNode89) => {90 const next = new Map(portalMap);91 const portal = next.get(hostName) ?? new Map<string, React.ReactNode>();92 const updatedPortal = new Map(portal);93 updatedPortal.set(name, content);94 next.set(hostName, updatedPortal);95 portalMap = next;96 notifyListeners();97};9899const removePortal = (hostName: string, name: string) => {100 const next = new Map(portalMap);101 const portal = next.get(hostName);102 if (portal) {103 const updatedPortal = new Map(portal);104 updatedPortal.delete(name);105 if (updatedPortal.size === 0) {106 next.delete(hostName);107 } else {108 next.set(hostName, updatedPortal);109 }110 portalMap = next;111 notifyListeners();112 }113};
What it pulls in
npm packages
Files it writes
More from tetra-ui
All 41 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordionaccordion | tetra-ui | Components | Free | 1 dep | |
| Action Inputaction-input | tetra-ui | Components | Free | no deps | |
| Alertalert | tetra-ui | Components | Free | no deps | |
| Avataravatar | tetra-ui | Components | Free | no deps | |
| Badgebadge | tetra-ui | Components | Free | no deps | |
| Bottom Sheetbottom-sheet | tetra-ui | Components | Free | 4 deps · 7 files | |
| Buttonbutton | tetra-ui | Components | Free | no deps | |
| Cardcard | tetra-ui | Components | Free | no deps |
