🐛(frontend) unmount PiP portal synchronously on pagehide

When the PiP window closes, the browser destroys its document right
after `pagehide`. If the portal unmount is left to React's async
scheduling, it commits against a dead document and `removeChild`
throws "NotFoundError", crashing the app.

Subscribe `PictureInPicturePortal` to the Valtio store with
`sync: true`, and use `flushSync` in `usePictureInPicture` on
teardown so React unmounts the portal while the PiP document is
still alive.

Fix 019f42cf-86a9-7ad2-8e64-81b004ddc5de
This commit is contained in:
lebaudantoine
2026-08-06 17:55:29 +02:00
committed by aleb_the_flash
parent 23bb3c39d0
commit b8958e6e87
2 changed files with 8 additions and 3 deletions
@@ -7,7 +7,9 @@ import { useEffect, useMemo } from 'react'
import { CrossDocumentOverlaysContext } from '@/primitives/CrossDocumentOverlaysContext'
const InternalPortal = ({ children }: { children: React.ReactNode }) => {
const pipStoreSnap = useSnapshot(documentPictureInPictureStore)
const pipStoreSnap = useSnapshot(documentPictureInPictureStore, {
sync: true,
})
const container = useMemo(() => {
return pipStoreSnap?.window?.document.getElementById('root')
@@ -19,7 +21,7 @@ const InternalPortal = ({ children }: { children: React.ReactNode }) => {
}
}, [])
if (!container) return null
if (!container || !container.isConnected) return null
return createPortal(
/**
@@ -1,5 +1,6 @@
import { ref, useSnapshot } from 'valtio'
import { useCallback, useMemo } from 'react'
import { flushSync } from 'react-dom'
import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture'
import { useTranslation } from 'react-i18next'
@@ -73,7 +74,9 @@ export const usePictureInPicture = () => {
const cleanUp = () => {
if (documentPictureInPictureStore.window === pipWindow) {
documentPictureInPictureStore.window = null
flushSync(() => {
documentPictureInPictureStore.window = null
})
}
}
pipWindow.addEventListener('pagehide', () => cleanUp(), { once: true })