Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/keyed-portal-host.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion packages/portal/src/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, React.ReactNode>();
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]) => (
<React.Fragment key={portalName}>{children}</React.Fragment>
))}
</>
);
}

export function Portal({
Expand Down