diff --git a/.changeset/keyed-portal-host.md b/.changeset/keyed-portal-host.md new file mode 100644 index 00000000..85354d89 --- /dev/null +++ b/.changeset/keyed-portal-host.md @@ -0,0 +1,5 @@ +--- +'@rn-primitives/portal': patch +--- + +Fix `PortalHost` scrambling the remaining portals when one portal unmounts. Portals were rendered as a keyless fragment, so React reconciled them by array position: removing a portal slid every later portal onto its predecessor's component instances, silently transferring state between same-type portals (e.g. two bottom sheets or dialogs) and remounting diverging ones. Each portal's subtree is now keyed by its portal name, making removals local to the removed portal. diff --git a/packages/portal/src/portal.tsx b/packages/portal/src/portal.tsx index 71f9482e..e1789023 100644 --- a/packages/portal/src/portal.tsx +++ b/packages/portal/src/portal.tsx @@ -33,7 +33,17 @@ const removePortal = (hostName: string, name: string) => { export function PortalHost({ name = DEFAULT_PORTAL_HOST }: { name?: string }) { const portalMap = usePortal((state) => state.map).get(name) ?? new Map(); if (portalMap.size === 0) return null; - return <>{Array.from(portalMap.values())}; + // Key each portal's subtree by its portal name. A keyless fragment makes React + // reconcile portals by array position, so removing one portal slides every later + // portal onto its predecessor's component instances: same-type nodes silently + // inherit the predecessor's state and diverging nodes are torn down and remounted. + return ( + <> + {Array.from(portalMap.entries()).map(([portalName, children]) => ( + {children} + ))} + + ); } export function Portal({