diff --git a/.github/ISSUE_TEMPLATE/compiler_bug_report.yml b/.github/ISSUE_TEMPLATE/compiler_bug_report.yml index 233201d3f5b..a2c0e3d7aa3 100644 --- a/.github/ISSUE_TEMPLATE/compiler_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/compiler_bug_report.yml @@ -11,7 +11,7 @@ body: options: - label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization) - label: babel-plugin-react-compiler (build issue installing or using the Babel plugin) - - label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin) + - label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin) - label: react-compiler-healthcheck (build issue installing or using the healthcheck script) - type: input attributes: diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 18cf6124444..e17729c032a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -9433,4 +9433,34 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue , ); }); + + // @gate enableCPUSuspense + it('outlines deferred Suspense boundaries', async () => { + function Log({text}) { + Scheduler.log(text); + return text; + } + + await act(async () => { + renderToPipeableStream( +
+ }> + {} + +
, + ).pipe(writable); + await jest.runAllTimers(); + const temp = document.createElement('body'); + temp.innerHTML = buffer; + expect(getVisibleChildren(temp)).toEqual(
Waiting
); + }); + + assertLog(['Waiting', 'hello']); + + expect(getVisibleChildren(container)).toEqual( +
+ hello +
, + ); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5875455f34c..7020af6e043 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2456,10 +2456,7 @@ function updateSuspenseComponent( } return bailoutOffscreenComponent(null, primaryChildFragment); - } else if ( - enableCPUSuspense && - typeof nextProps.unstable_expectedLoadTime === 'number' - ) { + } else if (enableCPUSuspense && nextProps.defer === true) { // This is a CPU-bound tree. Skip this tree and show a placeholder to // unblock the surrounding content. Then immediately retry after the // initial commit. diff --git a/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js b/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js index 1b1489de33a..54e4207c656 100644 --- a/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js @@ -1,3 +1,5 @@ +/* eslint-disable react/jsx-boolean-value */ + let React; let ReactNoop; let Scheduler; @@ -122,9 +124,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { <>
- }> + }>
@@ -164,9 +164,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { <>
- }> + }>
@@ -209,9 +207,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { <>
- }> + }>
@@ -263,14 +259,10 @@ describe('ReactSuspenseWithNoopRenderer', () => { <>
- }> + }>
- }> + }>
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 531f1202762..b439607e158 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -181,6 +181,7 @@ import { enableViewTransition, enableFizzBlockingRender, enableAsyncDebugInfo, + enableCPUSuspense, } from 'shared/ReactFeatureFlags'; import assign from 'shared/assign'; @@ -250,6 +251,7 @@ type SuspenseBoundary = { row: null | SuspenseListRow, // the row that this boundary blocks from completing. completedSegments: Array, // completed but not yet flushed segments. byteSize: number, // used to determine whether to inline children boundaries. + defer: boolean, // never inline deferred boundaries fallbackAbortableTasks: Set, // used to cancel task on the fallback if the boundary completes or gets canceled. contentState: HoistableState, fallbackState: HoistableState, @@ -456,7 +458,9 @@ function isEligibleForOutlining( // The larger this limit is, the more we can save on preparing fallbacks in case we end up // outlining. return ( - (boundary.byteSize > 500 || hasSuspenseyContent(boundary.contentState)) && + (boundary.byteSize > 500 || + hasSuspenseyContent(boundary.contentState) || + boundary.defer) && // For boundaries that can possibly contribute to the preamble we don't want to outline // them regardless of their size since the fallbacks should only be emitted if we've // errored the boundary. @@ -782,6 +786,7 @@ function createSuspenseBoundary( fallbackAbortableTasks: Set, contentPreamble: null | Preamble, fallbackPreamble: null | Preamble, + defer: boolean, ): SuspenseBoundary { const boundary: SuspenseBoundary = { status: PENDING, @@ -791,6 +796,7 @@ function createSuspenseBoundary( row: row, completedSegments: [], byteSize: 0, + defer: defer, fallbackAbortableTasks, errorDigest: null, contentState: createHoistableState(), @@ -1274,6 +1280,7 @@ function renderSuspenseBoundary( // in case it ends up generating a large subtree of content. const fallback: ReactNodeList = props.fallback; const content: ReactNodeList = props.children; + const defer: boolean = enableCPUSuspense && props.defer === true; const fallbackAbortSet: Set = new Set(); let newBoundary: SuspenseBoundary; @@ -1284,6 +1291,7 @@ function renderSuspenseBoundary( fallbackAbortSet, createPreambleState(), createPreambleState(), + defer, ); } else { newBoundary = createSuspenseBoundary( @@ -1292,6 +1300,7 @@ function renderSuspenseBoundary( fallbackAbortSet, null, null, + defer, ); } if (request.trackedPostpones !== null) { @@ -1327,29 +1336,32 @@ function renderSuspenseBoundary( // no parent segment so there's nothing to wait on. contentRootSegment.parentFlushed = true; - if (request.trackedPostpones !== null) { + const trackedPostpones = request.trackedPostpones; + if (trackedPostpones !== null || defer) { + // This is a prerender or deferred boundary. In this mode we want to render the fallback synchronously + // and schedule the content to render later. This is the opposite of what we do during a normal render + // where we try to skip rendering the fallback if the content itself can render synchronously + // Stash the original stack frame. const suspenseComponentStack = task.componentStack; - // This is a prerender. In this mode we want to render the fallback synchronously and schedule - // the content to render later. This is the opposite of what we do during a normal render - // where we try to skip rendering the fallback if the content itself can render synchronously - const trackedPostpones = request.trackedPostpones; const fallbackKeyPath: KeyNode = [ keyPath[0], 'Suspense Fallback', keyPath[2], ]; - const fallbackReplayNode: ReplayNode = [ - fallbackKeyPath[1], - fallbackKeyPath[2], - ([]: Array), - null, - ]; - trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode); - // We are rendering the fallback before the boundary content so we keep track of - // the fallback replay node until we determine if the primary content suspends - newBoundary.trackedFallbackNode = fallbackReplayNode; + if (trackedPostpones !== null) { + const fallbackReplayNode: ReplayNode = [ + fallbackKeyPath[1], + fallbackKeyPath[2], + ([]: Array), + null, + ]; + trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode); + // We are rendering the fallback before the boundary content so we keep track of + // the fallback replay node until we determine if the primary content suspends + newBoundary.trackedFallbackNode = fallbackReplayNode; + } task.blockedSegment = boundarySegment; task.blockedPreamble = newBoundary.fallbackPreamble; @@ -1580,6 +1592,7 @@ function replaySuspenseBoundary( const content: ReactNodeList = props.children; const fallback: ReactNodeList = props.fallback; + const defer: boolean = enableCPUSuspense && props.defer === true; const fallbackAbortSet: Set = new Set(); let resumedBoundary: SuspenseBoundary; @@ -1590,6 +1603,7 @@ function replaySuspenseBoundary( fallbackAbortSet, createPreambleState(), createPreambleState(), + defer, ); } else { resumedBoundary = createSuspenseBoundary( @@ -1598,6 +1612,7 @@ function replaySuspenseBoundary( fallbackAbortSet, null, null, + defer, ); } resumedBoundary.parentFlushed = true; @@ -4384,6 +4399,7 @@ function abortRemainingSuspenseBoundary( new Set(), null, null, + false, ); resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender. @@ -5493,7 +5509,8 @@ function flushSegment( !flushingPartialBoundaries && isEligibleForOutlining(request, boundary) && (flushedByteSize + boundary.byteSize > request.progressiveChunkSize || - hasSuspenseyContent(boundary.contentState)) + hasSuspenseyContent(boundary.contentState) || + boundary.defer) ) { // Inlining this boundary would make the current sequence being written too large // and block the parent for too long. Instead, it will be emitted separately so that we diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 0b8d222e5cd..bcdda6da2a7 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -312,7 +312,7 @@ export type SuspenseProps = { suspenseCallback?: (Set | null) => mixed, unstable_avoidThisFallback?: boolean, - unstable_expectedLoadTime?: number, + defer?: boolean, name?: string, };