diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 519484f27222..8d1904097344 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -2337,7 +2337,7 @@ function preinitStyle( getStylesheetSelectorFromKey(key), ); if (instance) { - state.loading = Loaded; + state.loading = Loaded & Inserted; } else { // Construct a new instance and insert it const stylesheetProps = Object.assign( @@ -2769,6 +2769,7 @@ export function acquireResource( getStylesheetSelectorFromKey(key), ); if (instance) { + resource.state.loading |= Inserted; resource.instance = instance; markNodeAsHoistable(instance); return instance; @@ -3360,71 +3361,73 @@ export function suspendResource( return; } } - if (resource.instance === null) { - const qualifiedProps: StylesheetQualifyingProps = props; - const key = getStyleKey(qualifiedProps.href); + if ((resource.state.loading & Inserted) === NotLoaded) { + if (resource.instance === null) { + const qualifiedProps: StylesheetQualifyingProps = props; + const key = getStyleKey(qualifiedProps.href); - // Attempt to hydrate instance from DOM - let instance: null | Instance = hoistableRoot.querySelector( - getStylesheetSelectorFromKey(key), - ); - if (instance) { - // If this instance has a loading state it came from the Fizz runtime. - // If there is not loading state it is assumed to have been server rendered - // as part of the preamble and therefore synchronously loaded. It could have - // errored however which we still do not yet have a means to detect. For now - // we assume it is loaded. - const maybeLoadingState: ?Promise = (instance: any)._p; - if ( - maybeLoadingState !== null && - typeof maybeLoadingState === 'object' && - // $FlowFixMe[method-unbinding] - typeof maybeLoadingState.then === 'function' - ) { - const loadingState = maybeLoadingState; - state.count++; - const ping = onUnsuspend.bind(state); - loadingState.then(ping, ping); + // Attempt to hydrate instance from DOM + let instance: null | Instance = hoistableRoot.querySelector( + getStylesheetSelectorFromKey(key), + ); + if (instance) { + // If this instance has a loading state it came from the Fizz runtime. + // If there is not loading state it is assumed to have been server rendered + // as part of the preamble and therefore synchronously loaded. It could have + // errored however which we still do not yet have a means to detect. For now + // we assume it is loaded. + const maybeLoadingState: ?Promise = (instance: any)._p; + if ( + maybeLoadingState !== null && + typeof maybeLoadingState === 'object' && + // $FlowFixMe[method-unbinding] + typeof maybeLoadingState.then === 'function' + ) { + const loadingState = maybeLoadingState; + state.count++; + const ping = onUnsuspend.bind(state); + loadingState.then(ping, ping); + } + resource.state.loading |= Inserted; + resource.instance = instance; + markNodeAsHoistable(instance); + return; } - resource.state.loading |= Inserted; - resource.instance = instance; - markNodeAsHoistable(instance); - return; - } - const ownerDocument = getDocumentFromRoot(hoistableRoot); - - const stylesheetProps = stylesheetPropsFromRawProps(props); - const preloadProps = preloadPropsMap.get(key); - if (preloadProps) { - adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); - } + const ownerDocument = getDocumentFromRoot(hoistableRoot); - // Construct and insert a new instance - instance = ownerDocument.createElement('link'); - markNodeAsHoistable(instance); - const linkInstance: HTMLLinkElement = (instance: any); - // This Promise is a loading state used by the Fizz runtime. We need this incase there is a race - // between this resource being rendered on the client and being rendered with a late completed boundary. - (linkInstance: any)._p = new Promise((resolve, reject) => { - linkInstance.onload = resolve; - linkInstance.onerror = reject; - }); - setInitialProperties(instance, 'link', stylesheetProps); - resource.instance = instance; - } + const stylesheetProps = stylesheetPropsFromRawProps(props); + const preloadProps = preloadPropsMap.get(key); + if (preloadProps) { + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); + } - if (state.stylesheets === null) { - state.stylesheets = new Map(); - } - state.stylesheets.set(resource, hoistableRoot); + // Construct and insert a new instance + instance = ownerDocument.createElement('link'); + markNodeAsHoistable(instance); + const linkInstance: HTMLLinkElement = (instance: any); + // This Promise is a loading state used by the Fizz runtime. We need this incase there is a race + // between this resource being rendered on the client and being rendered with a late completed boundary. + (linkInstance: any)._p = new Promise((resolve, reject) => { + linkInstance.onload = resolve; + linkInstance.onerror = reject; + }); + setInitialProperties(instance, 'link', stylesheetProps); + resource.instance = instance; + } - const preloadEl = resource.state.preload; - if (preloadEl && (resource.state.loading & Settled) === NotLoaded) { - state.count++; - const ping = onUnsuspend.bind(state); - preloadEl.addEventListener('load', ping); - preloadEl.addEventListener('error', ping); + if (state.stylesheets === null) { + state.stylesheets = new Map(); + } + state.stylesheets.set(resource, hoistableRoot); + + const preloadEl = resource.state.preload; + if (preloadEl && (resource.state.loading & Settled) === NotLoaded) { + state.count++; + const ping = onUnsuspend.bind(state); + preloadEl.addEventListener('load', ping); + preloadEl.addEventListener('error', ping); + } } } } diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 8fc6ef45ab2e..622722a947d7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -2852,6 +2852,84 @@ body { ]); }); + // https://github.com/facebook/react/issues/27585 + it('does not reinsert already inserted stylesheets during a delayed commit', async () => { + await act(() => { + renderToPipeableStream( + + + + + server + + , + ).pipe(writable); + }); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + server + , + ); + + const root = ReactDOMClient.createRoot(document.body); + expect(getMeaningfulChildren(container)).toBe(undefined); + root.render( + <> + + +
client
+ , + ); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + +
client
+ + , + ); + + // In a transition we add another reference to an already loaded resource + // https://github.com/facebook/react/issues/27585 + React.startTransition(() => { + root.render( + <> + + +
client
+ + , + ); + }); + await waitForAll([]); + // In https://github.com/facebook/react/issues/27585 the order updated + // to second, third, first + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + +
client
+ + , + ); + }); + xit('can delay commit until css resources error', async () => { // TODO: This test fails and crashes jest. need to figure out why before unskipping. const root = ReactDOMClient.createRoot(container);