diff --git a/src/components/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx index 2954b3f4..cef7a023 100644 --- a/src/components/bottomSheetModal/BottomSheetModal.tsx +++ b/src/components/bottomSheetModal/BottomSheetModal.tsx @@ -272,6 +272,23 @@ function BottomSheetModalComponent( }); } + /** + * 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. @@ -362,7 +379,7 @@ function BottomSheetModalComponent( //#region callbacks const handlePortalOnUnmount = useCallback( - function handlePortalOnUnmount() { + function handlePortalOnUnmount(removePortalFromHost?: () => void) { if (__DEV__) { print({ component: 'BottomSheetModal', @@ -374,6 +391,27 @@ function BottomSheetModalComponent( } 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; }