diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 2fcf5bf9a5..6470dcf14c 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -2358,6 +2358,7 @@ export function getResource(
type: string,
currentProps: any,
pendingProps: any,
+ currentResource: null | Resource,
): null | Resource {
const resourceRoot = getCurrentResourceRoot();
if (!resourceRoot) {
@@ -2430,9 +2431,44 @@ export function getResource(
);
}
}
+ if (currentProps && currentResource === null) {
+ // This node was previously an Instance type and is becoming a Resource type
+ // For now we error because we don't support flavor changes
+ let diff = '';
+ if (__DEV__) {
+ diff = `
+
+ - ${describeLinkForResourceErrorDEV(currentProps)}
+ + ${describeLinkForResourceErrorDEV(pendingProps)}`;
+ }
+ throw new Error(
+ 'Expected not to update to be updated to a stylehsheet with precedence.' +
+ ' Check the `rel`, `href`, and `precedence` props of this component.' +
+ ' Alternatively, check whether two different components render in the same slot or share the same key.' +
+ diff,
+ );
+ }
return resource;
+ } else {
+ if (currentProps && currentResource !== null) {
+ // This node was previously a Resource type and is becoming an Instance type
+ // For now we error because we don't support flavor changes
+ let diff = '';
+ if (__DEV__) {
+ diff = `
+
+ - ${describeLinkForResourceErrorDEV(currentProps)}
+ + ${describeLinkForResourceErrorDEV(pendingProps)}`;
+ }
+ throw new Error(
+ 'Expected stylesheet with precedence to not be updated to a different kind of .' +
+ ' Check the `rel`, `href`, and `precedence` props of this component.' +
+ ' Alternatively, check whether two different components render in the same slot or share the same key.' +
+ diff,
+ );
+ }
+ return null;
}
- return null;
}
case 'script': {
const async = pendingProps.async;
@@ -2473,6 +2509,49 @@ export function getResource(
}
}
+function describeLinkForResourceErrorDEV(props: any) {
+ if (__DEV__) {
+ let describedProps = 0;
+
+ let description = ' describedProps) {
+ description += ' ...';
+ }
+ description += ' />';
+ return description;
+ }
+ return '';
+}
+
function styleTagPropsFromRawProps(
rawProps: StyleTagQualifyingProps,
): StyleTagProps {
diff --git a/packages/react-dom/src/__tests__/ReactDOMHostComponentTransitions-test.js b/packages/react-dom/src/__tests__/ReactDOMHostComponentTransitions-test.js
new file mode 100644
index 0000000000..1a484dba60
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMHostComponentTransitions-test.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
+ */
+
+'use strict';
+
+let JSDOM;
+let React;
+let ReactDOMClient;
+let container;
+let waitForAll;
+
+describe('ReactDOM HostSingleton', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ JSDOM = require('jsdom').JSDOM;
+ // Test Environment
+ const jsdom = new JSDOM(
+ '
,
+ );
+ await waitForAll([]);
+ if (__DEV__) {
+ expect(errors).toEqual([
+ `Expected not to update to be updated to a stylehsheet with precedence. Check the \`rel\`, \`href\`, and \`precedence\` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.
+
+ -
+ + `,
+ ]);
+ } else {
+ expect(errors).toEqual([
+ 'Expected not to update to be updated to a stylehsheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.',
+ ]);
+ }
+ });
+
+ it('errors when a hoistable Resource becomes an instance', async () => {
+ const errors = [];
+ function onError(e) {
+ errors.push(e.message);
+ }
+ const root = ReactDOMClient.createRoot(container, {
+ onUncaughtError: onError,
+ });
+
+ root.render(
+
+
+
,
+ );
+ await waitForAll([]);
+ const event = new window.Event('load');
+ const preloads = document.querySelectorAll('link[rel="preload"]');
+ for (let i = 0; i < preloads.length; i++) {
+ const node = preloads[i];
+ node.dispatchEvent(event);
+ }
+ const stylesheets = document.querySelectorAll('link[rel="preload"]');
+ for (let i = 0; i < stylesheets.length; i++) {
+ const node = stylesheets[i];
+ node.dispatchEvent(event);
+ }
+
+ root.render(
+
+
+
,
+ );
+ await waitForAll([]);
+ if (__DEV__) {
+ expect(errors).toEqual([
+ `Expected stylesheet with precedence to not be updated to a different kind of . Check the \`rel\`, \`href\`, and \`precedence\` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.
+
+ -
+ + `,
+ ]);
+ } else {
+ expect(errors).toEqual([
+ 'Expected stylesheet with precedence to not be updated to a different kind of . Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.',
+ ]);
+ }
+ });
+});
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 6eeb7ab377..793a3fa942 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -1689,22 +1689,36 @@ function updateHostHoistable(
renderLanes: Lanes,
) {
markRef(current, workInProgress);
- const currentProps = current === null ? null : current.memoizedProps;
- const resource = (workInProgress.memoizedState = getResource(
- workInProgress.type,
- currentProps,
- workInProgress.pendingProps,
- ));
+
if (current === null) {
- if (!getIsHydrating() && resource === null) {
- // This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
- workInProgress.stateNode = createHoistableInstance(
- workInProgress.type,
- workInProgress.pendingProps,
- getRootHostContainer(),
- workInProgress,
- );
+ const resource = getResource(
+ workInProgress.type,
+ null,
+ workInProgress.pendingProps,
+ null,
+ );
+ if (resource) {
+ workInProgress.memoizedState = resource;
+ } else {
+ if (!getIsHydrating()) {
+ // This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
+ workInProgress.stateNode = createHoistableInstance(
+ workInProgress.type,
+ workInProgress.pendingProps,
+ getRootHostContainer(),
+ workInProgress,
+ );
+ }
}
+ } else {
+ // Get Resource may or may not return a resource. either way we stash the result
+ // on memoized state.
+ workInProgress.memoizedState = getResource(
+ workInProgress.type,
+ current.memoizedProps,
+ workInProgress.pendingProps,
+ current.memoizedState,
+ );
}
// Resources never have reconciler managed children. It is possible for
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index a060d0f00a..4a671940ba 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -1052,7 +1052,6 @@ function completeWork(
return null;
} else {
// This is a Hoistable Instance
-
// This must come at the very end of the complete phase.
bubbleProperties(workInProgress);
preloadInstanceAndSuspendIfNeeded(
@@ -1064,21 +1063,18 @@ function completeWork(
return null;
}
} else {
- // We are updating.
- const currentResource = current.memoizedState;
- if (nextResource !== currentResource) {
- // We are transitioning to, from, or between Hoistable Resources
- // and require an update
- markUpdate(workInProgress);
- }
- if (nextResource !== null) {
- // This is a Hoistable Resource
- // This must come at the very end of the complete phase.
-
- bubbleProperties(workInProgress);
- if (nextResource === currentResource) {
- workInProgress.flags &= ~MaySuspendCommit;
- } else {
+ // This is an update.
+ if (nextResource) {
+ // This is a Resource
+ if (nextResource !== current.memoizedState) {
+ // we have a new Resource. we need to update
+ markUpdate(workInProgress);
+ // This must come at the very end of the complete phase.
+ bubbleProperties(workInProgress);
+ // This must come at the very end of the complete phase, because it might
+ // throw to suspend, and if the resource immediately loads, the work loop
+ // will resume rendering as if the work-in-progress completed. So it must
+ // fully complete.
preloadResourceAndSuspendIfNeeded(
workInProgress,
nextResource,
@@ -1086,10 +1082,15 @@ function completeWork(
newProps,
renderLanes,
);
+ return null;
+ } else {
+ // This must come at the very end of the complete phase.
+ bubbleProperties(workInProgress);
+ workInProgress.flags &= ~MaySuspendCommit;
+ return null;
}
- return null;
} else {
- // This is a Hoistable Instance
+ // This is an Instance
// We may have props to update on the Hoistable instance.
if (supportsMutation) {
const oldProps = current.memoizedProps;
@@ -1107,7 +1108,6 @@ function completeWork(
renderLanes,
);
}
-
// This must come at the very end of the complete phase.
bubbleProperties(workInProgress);
preloadInstanceAndSuspendIfNeeded(
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 9bb82658ac..221c62a871 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -512,5 +512,7 @@
"524": "Values cannot be passed to next() of AsyncIterables passed to Client Components.",
"525": "A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the \"react\" package is used.\n- A library pre-bundled an old copy of \"react\" or \"react/jsx-runtime\".\n- A compiler tries to \"inline\" JSX instead of using the runtime.",
"526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.",
- "527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s\nLearn more: https://react.dev/warnings/version-mismatch"
+ "527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s\nLearn more: https://react.dev/warnings/version-mismatch",
+ "528": "Expected not to update to be updated to a stylehsheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s",
+ "529": "Expected stylesheet with precedence to not be updated to a different kind of . Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s"
}