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
+
+
resolved 1
+
+
, + ); + + 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
+
+
resolved 1
+
+
, + ); + + 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
+
+
resolved 1
+
+ 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 =