Skip to content

fix(react): use useLayoutEffect for hasMounted to prevent source element race condition#1751

Open
pyto86pri wants to merge 1 commit intovidstack:mainfrom
pyto86pri:fix/react-media-outlet-source-race-condition
Open

fix(react): use useLayoutEffect for hasMounted to prevent source element race condition#1751
pyto86pri wants to merge 1 commit intovidstack:mainfrom
pyto86pri:fix/react-media-outlet-source-race-condition

Conversation

@pyto86pri
Copy link

Summary

  • Use useLayoutEffect instead of useEffect for setHasMounted(true) in MediaOutlet to prevent a race condition with hls.js's removeSourceChildren()

Problem

On iOS 17+, hls.js uses ManagedMediaSource which calls removeSourceChildren() during initialization, removing all <source> elements from the <video> element — including ones rendered by React (when !hasMounted).

The current useEffect for setHasMounted(true) triggers a batched re-render that may be deferred by React's scheduler (especially during unmount→remount transitions like Next.js page navigation). This allows the RAF-scheduled hls.js initialization to fire first, removing React's <source> elements before React can reconcile them, resulting in a NotFoundError from removeChild.

Timeline (before fix)

[commit]  <video><source>...</source></video>  (React tracks <source> in fiber tree)
[paint]   browser paint
[effect]  setHasMounted(true) → batched re-render scheduled
          React scheduler yields (>5ms due to unmount cleanup work)
[RAF]     hls.js init → removeSourceChildren() → removes React's <source>
[task]    React re-render → removeChild(<source>) → NotFoundError!

Timeline (after fix)

[commit]  <video><source>...</source></video>
[layout]  setHasMounted(true) → synchronous re-render → <source> removed
[paint]   browser paint
[RAF]     hls.js init → removeSourceChildren() → nothing to remove

Notes

  • SSR behavior is preserved: useLayoutEffect does not run on the server, so <source> elements are still rendered in SSR HTML for preloading
  • The useLayoutEffect re-render is lightweight (only removes <source> children), so no visible paint delay

Closes #1521

…ent race condition

On iOS 17+, hls.js uses ManagedMediaSource which calls removeSourceChildren()
to remove all <source> elements from the <video> element during initialization.
When this runs before React's batched re-render (triggered by setHasMounted)
commits, React tries to removeChild already-removed <source> elements, causing
a NotFoundError.

This race condition is especially likely during unmount→remount transitions
(e.g. page navigation in Next.js), where React's scheduler may yield before
processing the hasMounted re-render due to the additional cleanup work,
allowing the RAF-scheduled hls.js initialization to fire first.

Using useLayoutEffect ensures setHasMounted(true) triggers a synchronous
re-render during the commit phase, removing the <source> elements before
any RAF callbacks can fire.

Closes vidstack#1521

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pyto86pri pyto86pri closed this Feb 11, 2026
@pyto86pri pyto86pri deleted the fix/react-media-outlet-source-race-condition branch February 11, 2026 14:53
@pyto86pri pyto86pri restored the fix/react-media-outlet-source-race-condition branch February 12, 2026 15:32
@pyto86pri pyto86pri reopened this Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HLS with ManageMediaSource throws NotFoundError: The object can not be found here.

1 participant