Fix getResource short-circuit leaving stylesheet state.loading=NotLoaded after ReactDOM.preload#36358
Conversation
…oaded after ReactDOM.preload
When ReactDOM.preload(href, { as: 'style' }) ran before a fresh
<link rel="stylesheet" href={href} precedence> mounted inside a new
Suspense boundary, getResource would short-circuit on
preloadPropsMap.has(key), skip the preloadStylesheet call, and leave
state.loading at NotLoaded. completeWork would then throw a
SuspenseyCommitException and the boundary would paint its fallback
even though the preload bytes were already in cache.
Decouple the preloadStylesheet call from the preloadPropsMap.has()
gate. Reuse the stored props when the map already has an entry so we
still query for the matching <link rel="preload"> and update
state.loading accordingly.
|
Hi @gaojude! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Summary
When
ReactDOM.preload(href, { as: 'style' })runs before a fresh<link rel=\"stylesheet\" href={href} precedence>mounts inside a new<Suspense>boundary,getResourceshort-circuits onpreloadPropsMap.has(key), skips thepreloadStylesheetcall, and leavesstate.loadingatNotLoaded.completeWorkthen throwsSuspenseyCommitExceptionand the boundary paints its fallback even though the preload bytes are already in the browser's cache.Trace
In
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js, the stylesheet branch ofgetResourcelooked like this before the fix:The
preloadStylesheetcall is the only path that queries the existing<link rel=\"preload\" as=\"style\">and flipsstate.loadingtoLoaded. It is gated behind!preloadPropsMap.has(key), butpreloadPropsMapis populated unconditionally byReactDOM.preload(...)(theLhandler). So the sequenceReactDOM.preload(href, { as: 'style' })— populatespreloadPropsMap, inserts<link rel=\"preload\" as=\"style\"><link rel=\"stylesheet\" href={href} precedence>inside a fresh<Suspense>(=shellBoundary)getResourcecreates a fresh resource withstate.loading = NotLoadedquerySelectormisses (no<link rel=\"stylesheet\">in the document yet)preloadPropsMap.has(key) === true→preloadStylesheetnot called →state.loadingstays atNotLoadedcompleteWork→preloadResourceAndSuspendIfNeeded→ throwsSuspenseyCommitExceptionFix
Decouple the
preloadStylesheetcall from thepreloadPropsMap.has(key)gate. Reuse the stored props when the map already has an entry so we still query for the matching<link rel=\"preload\">and updatestate.loadingaccordingly:Repro
A minimal Next.js repro lives at vercel/next.js#93296. The fixture:
'use client'page that callsReactDOM.preload(href, { as: 'style' })for 3 hrefs inuseEffect<Link>to/logswhose layout has<Suspense>and whose page is a client component rendering<link rel=\"stylesheet\" href={href} precedence>for the same hrefsThe Playwright test asserts the fallback IS painted (bug fires) and a SuspenseComponent fiber catches
SuspenseyCommitException(flags & 16384set,updateQueueempty). With this fix applied, the boundary commits with content instead.Notes / open questions
ReactDOMFloat-test.js— the existing tests for this path are SSR + hydrate flows, and reproducing the client-only "preload-before-mount" sequence cleanly looks invasive. Happy to add one if there's a preferred shape.(existingPreloadProps: any)is becausepreloadPropsMapstoresPreloadProps | PreloadModuleProps. Open to a narrower type if preferred.