Skip to content
Open
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
40 changes: 39 additions & 1 deletion src/components/bottomSheetModal/BottomSheetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,23 @@ function BottomSheetModalComponent<T = never>(
});
}

/**
* if the modal was never presented, or has already been fully torn down
* (`unmount()` resets the status back to `INITIAL`), then there is nothing
* to dismiss and no inner sheet to close.
*
* Falling through would set the status to `DISMISSING` and call
* `forceClose()` on a `bottomSheetRef` that is still `null` — a silent
* no-op. Since nothing is animating, `onClose` never fires, so nothing ever
* transitions the status out of `DISMISSING`, and `handlePortalRender`
* suppresses every subsequent render. The modal is then permanently wedged:
* later `present()` calls mount the portal but render nothing, with no
* error or warning.
*/
if (statusRef.current === MODAL_STATUS.INITIAL) {
return;
}

/**
* if the modal position is already in a closed position,
* then we unmount the node and early exit.
Expand Down Expand Up @@ -362,7 +379,7 @@ function BottomSheetModalComponent<T = never>(

//#region callbacks
const handlePortalOnUnmount = useCallback(
function handlePortalOnUnmount() {
function handlePortalOnUnmount(removePortalFromHost?: () => void) {
if (__DEV__) {
print({
component: 'BottomSheetModal',
Expand All @@ -374,6 +391,27 @@ function BottomSheetModalComponent<T = never>(
}

if (statusRef.current === MODAL_STATUS.INITIAL) {
/**
* The sheet is already fully torn down — `unmount()` removed this portal
* from the host and reset the status.
*
* However `Portal`'s `handleOnUpdate` effect is keyed on `children`, whose
* element identity changes on every parent render. A trailing update can
* therefore land in the window between `unmount()`'s `removePortal` and
* this component actually unmounting, RE-ADDING the entry we just removed.
*
* Left behind, that orphan makes the host keep rendering the stale node,
* so the inner `BottomSheet` is never unmounted. The next `present()` then
* cannot open it: `handlePresent` reads a captured `mount` of `false`, so
* it skips the `snapToIndex` branch, while the still-mounted sheet already
* has `didAnimateOnMount === true` and so runs no mount animation either.
* The sheet silently never opens again, and every closed sheet leaks a
* live subtree.
*
* This is the last chance to drop that orphan. `removePortal` is
* idempotent, so this is a no-op when there is nothing stale to remove.
*/
removePortalFromHost?.();
return;
}

Expand Down