diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index dfe861142ab79..edf5814023142 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -1484,6 +1484,154 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(
Hello
);
});
+ // @gate experimental && enableSuspenseAvoidThisFallback
+ it('should respect unstable_avoidThisFallback', async () => {
+ const resolved = {
+ 0: false,
+ 1: false,
+ };
+ const promiseRes = {};
+ const promises = {
+ 0: new Promise(res => {
+ promiseRes[0] = () => {
+ resolved[0] = true;
+ res();
+ };
+ }),
+ 1: new Promise(res => {
+ promiseRes[1] = () => {
+ resolved[1] = true;
+ res();
+ };
+ }),
+ };
+
+ const InnerComponent = ({isClient, depth}) => {
+ if (isClient) {
+ // Resuspend after re-rendering on client to check that fallback shows on client
+ throw new Promise(() => {});
+ }
+ if (!resolved[depth]) {
+ throw promises[depth];
+ }
+ return (
+
+
+
+ );
+ };
+
+ function App({isClient}) {
+ return (
+
+
+
+
+
+ }
+ unstable_avoidThisFallback={true}>
+
+
+ }>
+
+
+
+ }
+ unstable_avoidThisFallback={true}>
+
+
+
+
+
+
+ );
+ }
+
+ await jest.runAllTimers();
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+
+ // Nothing is output since root has a suspense with avoidedThisFallback that hasn't resolved
+ expect(getVisibleChildren(container)).toEqual(undefined);
+ expect(container.innerHTML).not.toContain('Avoided Fallback');
+
+ // resolve first suspense component with avoidThisFallback
+ await act(async () => {
+ promiseRes[0]();
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+
+ Non Suspense Content
+
resolved 0
+
Fallback
+
,
+ );
+
+ expect(container.innerHTML).not.toContain('Avoided Fallback2');
+
+ await act(async () => {
+ promiseRes[1]();
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+
+ Non Suspense Content
+
resolved 0
+
+
,
+ );
+
+ let root;
+ await act(async () => {
+ root = ReactDOM.hydrateRoot(container, );
+ Scheduler.unstable_flushAll();
+ await jest.runAllTimers();
+ });
+
+ // No change after hydration
+ expect(getVisibleChildren(container)).toEqual(
+
+ Non Suspense Content
+
resolved 0
+
+
,
+ );
+
+ await act(async () => {
+ // Trigger update by changing isClient to true
+ root.render();
+ Scheduler.unstable_flushAll();
+ await jest.runAllTimers();
+ });
+
+ // Now that we've resuspended at the root we show the root fallback
+ expect(getVisibleChildren(container)).toEqual(
+
+ Non Suspense Content
+
resolved 0
+
+
Avoided Fallback
+
,
+ );
+ });
+
// @gate supportsNativeUseSyncExternalStore
// @gate experimental
it('calls getServerSnapshot instead of getSnapshot', async () => {
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 58ddbeea3f7ba..1b2bbc28f6669 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -1480,10 +1480,21 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
);
const endSuspenseBoundary = stringToPrecomputedChunk('');
+export function pushStartCompletedSuspenseBoundary(
+ target: Array,
+) {
+ target.push(startCompletedSuspenseBoundary);
+}
+
+export function pushEndCompletedSuspenseBoundary(
+ target: Array,
+) {
+ target.push(endSuspenseBoundary);
+}
+
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startCompletedSuspenseBoundary);
}
@@ -1497,7 +1508,6 @@ export function writeStartPendingSuspenseBoundary(
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startClientRenderedSuspenseBoundary);
}
diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
index 3dc354b452319..60ab7544d88fa 100644
--- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
@@ -86,6 +86,8 @@ export {
pushEmpty,
pushStartInstance,
pushEndInstance,
+ pushStartCompletedSuspenseBoundary,
+ pushEndCompletedSuspenseBoundary,
writeStartSegment,
writeEndSegment,
writeCompletedSegmentInstruction,
@@ -116,23 +118,17 @@ export function pushTextInstance(
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
if (responseState.generateStaticMarkup) {
// A completed boundary is done and doesn't need a representation in the HTML
// if we're not going to be hydrating it.
return true;
}
- return writeStartCompletedSuspenseBoundaryImpl(
- destination,
- responseState,
- id,
- );
+ return writeStartCompletedSuspenseBoundaryImpl(destination, responseState);
}
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
if (responseState.generateStaticMarkup) {
// A client rendered boundary is done and doesn't need a representation in the HTML
@@ -142,7 +138,6 @@ export function writeStartClientRenderedSuspenseBoundary(
return writeStartClientRenderedSuspenseBoundaryImpl(
destination,
responseState,
- id,
);
}
export function writeEndCompletedSuspenseBoundary(
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index 9f7501007ed87..24e9da45423d2 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -204,11 +204,16 @@ export function writePlaceholder(
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
- writeChunk(destination, SUSPENSE_COMPLETE);
- return writeChunk(destination, formatID(id));
+ return writeChunk(destination, SUSPENSE_COMPLETE);
}
+
+export function pushStartCompletedSuspenseBoundary(
+ target: Array,
+): void {
+ target.push(SUSPENSE_COMPLETE);
+}
+
export function writeStartPendingSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
@@ -220,10 +225,8 @@ export function writeStartPendingSuspenseBoundary(
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
- id: SuspenseBoundaryID,
): boolean {
- writeChunk(destination, SUSPENSE_CLIENT_RENDER);
- return writeChunk(destination, formatID(id));
+ return writeChunk(destination, SUSPENSE_CLIENT_RENDER);
}
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
@@ -231,6 +234,11 @@ export function writeEndCompletedSuspenseBoundary(
): boolean {
return writeChunk(destination, END);
}
+export function pushEndCompletedSuspenseBoundary(
+ target: Array,
+): void {
+ target.push(END);
+}
export function writeEndPendingSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 4b7eb46d94b48..f19f45e41f182 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -52,6 +52,8 @@ import {
pushTextInstance,
pushStartInstance,
pushEndInstance,
+ pushStartCompletedSuspenseBoundary,
+ pushEndCompletedSuspenseBoundary,
createSuspenseBoundaryID,
getChildFormatContext,
} from './ReactServerFormatConfig';
@@ -107,6 +109,7 @@ import {
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableLazyElements,
+ enableSuspenseAvoidThisFallback,
} from 'shared/ReactFeatureFlags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -520,6 +523,23 @@ function renderSuspenseBoundary(
popComponentStackInDEV(task);
}
+function renderBackupSuspenseBoundary(
+ request: Request,
+ task: Task,
+ props: Object,
+) {
+ pushBuiltInComponentStackInDEV(task, 'Suspense');
+
+ const content = props.children;
+ const segment = task.blockedSegment;
+
+ pushStartCompletedSuspenseBoundary(segment.chunks);
+ renderNode(request, task, content);
+ pushEndCompletedSuspenseBoundary(segment.chunks);
+
+ popComponentStackInDEV(task);
+}
+
function renderHostElement(
request: Request,
task: Task,
@@ -986,7 +1006,14 @@ function renderElement(
}
// eslint-disable-next-line-no-fallthrough
case REACT_SUSPENSE_TYPE: {
- renderSuspenseBoundary(request, task, props);
+ if (
+ enableSuspenseAvoidThisFallback &&
+ props.unstable_avoidThisFallback === true
+ ) {
+ renderBackupSuspenseBoundary(request, task, props);
+ } else {
+ renderSuspenseBoundary(request, task, props);
+ }
return;
}
}
@@ -1604,7 +1631,6 @@ function flushSegment(
writeStartClientRenderedSuspenseBoundary(
destination,
request.responseState,
- boundary.id,
);
// Flush the fallback.
@@ -1658,12 +1684,7 @@ function flushSegment(
return writeEndPendingSuspenseBoundary(destination, request.responseState);
} else {
// We can inline this boundary's content as a complete boundary.
-
- writeStartCompletedSuspenseBoundary(
- destination,
- request.responseState,
- boundary.id,
- );
+ writeStartCompletedSuspenseBoundary(destination, request.responseState);
const completedSegments = boundary.completedSegments;
invariant(
diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
index 5458a4f50a9d5..22539f3aa86bf 100644
--- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
@@ -39,6 +39,10 @@ export const pushEmpty = $$$hostConfig.pushEmpty;
export const pushTextInstance = $$$hostConfig.pushTextInstance;
export const pushStartInstance = $$$hostConfig.pushStartInstance;
export const pushEndInstance = $$$hostConfig.pushEndInstance;
+export const pushStartCompletedSuspenseBoundary =
+ $$$hostConfig.pushStartCompletedSuspenseBoundary;
+export const pushEndCompletedSuspenseBoundary =
+ $$$hostConfig.pushEndCompletedSuspenseBoundary;
export const writePlaceholder = $$$hostConfig.writePlaceholder;
export const writeStartCompletedSuspenseBoundary =
$$$hostConfig.writeStartCompletedSuspenseBoundary;
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index b9adc3b247617..608db694be518 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -101,6 +101,8 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
+
export const enableComponentStackLocations = true;
export const enableNewReconciler = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index a16e267d34f8f..9c3d45528c5af 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -49,6 +49,7 @@ export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 5e6e925e8d25b..bf96cc00e687c 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index a74e056faf5bc..0d2b226adf2f6 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index 028755cbe0cd3..f760425ce03cb 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -50,6 +50,7 @@ export const enableGetInspectorDataForInstanceInProduction = false;
export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableStrictEffects = false;
export const createRootStrictEffectsByDefault = false;
export const enableUseRefAccessWarning = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 103d02e8a6316..cb0efcc34bdcd 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 0f9e710eda92e..d1cfdbb749565 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index 740377f5aee86..62a10f475acaa 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
+export const enableSuspenseAvoidThisFallback = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
export const enableFilterEmptyStringAttributesDOM = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 3522244a3e13c..c5dde53578c95 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -48,6 +48,7 @@ export const enableProfilerNestedUpdateScheduledHook =
export const enableUpdaterTracking = __PROFILE__;
export const enableSuspenseLayoutEffectSemantics = true;
+export const enableSuspenseAvoidThisFallback = false;
// Logs additional User Timing API marks for use with an experimental profiling tool.
export const enableSchedulingProfiler =