diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 32745e4b109..93a16411d9f 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -9478,4 +9478,159 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
,
);
});
+
+ it('useId is consistent for siblings when component suspends with nested lazy', async () => {
+ // Inner component uses useId
+ function InnerComponent() {
+ const id = React.useId();
+ Scheduler.log('InnerComponent id: ' + id);
+ return inner;
+ }
+
+ // Outer component uses useId and renders a lazy inner
+ function OuterComponent({innerElement}) {
+ const id = React.useId();
+ Scheduler.log('OuterComponent id: ' + id);
+ return
{innerElement}
;
+ }
+
+ // This sibling also has useId - its ID must be consistent with server
+ function Sibling() {
+ const id = React.useId();
+ Scheduler.log('Sibling id: ' + id);
+ return sibling;
+ }
+
+ // Create fresh lazy components for SERVER (resolve immediately)
+ const serverLazyInner = React.lazy(async () => {
+ Scheduler.log('server lazy inner initializer');
+ return {default: };
+ });
+
+ const serverLazyOuter = React.lazy(async () => {
+ Scheduler.log('server lazy outer initializer');
+ return {
+ default: ,
+ };
+ });
+
+ // Server render with lazy (resolves immediately)
+ await act(() => {
+ const {pipe} = renderToPipeableStream(
+
+
+ <>{serverLazyOuter}>
+ <>
+
+ >
+
+ ,
+ );
+ pipe(writable);
+ });
+
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ inner
+
+ sibling
+
+ ,
+ );
+
+ assertLog([
+ 'server lazy outer initializer',
+ 'Sibling id: _R_2_',
+ 'OuterComponent id: _R_1_',
+ 'server lazy inner initializer',
+ 'InnerComponent id: _R_5_',
+ ]);
+
+ // Create fresh lazy components for CLIENT
+ let resolveClientInner;
+ const clientLazyInner = React.lazy(() => {
+ Scheduler.log('client lazy inner initializer');
+ const payload = {default: };
+ const promise = new Promise(r => {
+ resolveClientInner = () => {
+ promise.status = 'fulfilled';
+ promise.value = payload;
+ r(payload);
+ };
+ });
+ return promise;
+ });
+
+ let resolveClientOuter;
+ const clientLazyOuter = React.lazy(() => {
+ Scheduler.log('client lazy outer initializer');
+ const payload = {
+ default: ,
+ };
+ const promise = new Promise(r => {
+ resolveClientOuter = () => {
+ promise.status = 'fulfilled';
+ promise.value = payload;
+ r(payload);
+ };
+ });
+ return promise;
+ });
+
+ const hydrationErrors = [];
+
+ // Client hydrates with nested lazy components
+ let root;
+ React.startTransition(() => {
+ root = ReactDOMClient.hydrateRoot(
+ document,
+
+
+ <>{clientLazyOuter}>
+ <>
+
+ >
+
+ ,
+ {
+ onRecoverableError(error) {
+ hydrationErrors.push(error.message);
+ },
+ },
+ );
+ });
+
+ // First suspension on outer lazy
+ await waitFor(['client lazy outer initializer']);
+ resolveClientOuter();
+
+ // Second suspension on inner lazy
+ await waitFor([
+ 'OuterComponent id: _R_1_',
+ 'client lazy inner initializer',
+ ]);
+ resolveClientInner();
+
+ await waitForAll(['InnerComponent id: _R_5_', 'Sibling id: _R_2_']);
+
+ // The IDs should match the server-generated IDs
+ expect(hydrationErrors).toEqual([]);
+
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ inner
+
+ sibling
+
+ ,
+ );
+
+ root.unmount();
+ });
});
diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js
index cc43edc66b6..7fa1dcb8173 100644
--- a/packages/react-reconciler/src/ReactFiberFlags.js
+++ b/packages/react-reconciler/src/ReactFiberFlags.js
@@ -62,13 +62,13 @@ export const ShouldCapture = /* */ 0b0000000000000010000000000000
export const ForceUpdateForLegacySuspense = /* */ 0b0000000000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000000000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000000000010000000000000000000;
-export const Forked = /* */ 0b0000000000100000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
+export const Forked = /* */ 0b0000000000100000000000000000000;
export const SnapshotStatic = /* */ 0b0000000001000000000000000000000;
export const LayoutStatic = /* */ 0b0000000010000000000000000000000;
export const RefStatic = LayoutStatic;
@@ -142,4 +142,5 @@ export const StaticMask =
MaySuspendCommit |
ViewTransitionStatic |
ViewTransitionNamedStatic |
- PortalStatic;
+ PortalStatic |
+ Forked;