From 83564712b6e2907dcffdbf5f99b4713cf6c950de Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 15 Nov 2021 13:12:56 -0500 Subject: [PATCH] Move SuspenseList to experimental channel (#22765) There's more work to be done to implement this correctly on the server, so we're going to wait to release it until an 18.x minor. --- .../src/__tests__/ReactDOMFizzServer-test.js | 5 ++- ...DOMServerPartialHydration-test.internal.js | 9 +++- .../ReactDOMServerSuspense-test.internal.js | 9 +++- .../__tests__/ReactWrongReturnPointer-test.js | 5 ++- .../react-is/src/__tests__/ReactIs-test.js | 12 ++++-- .../__tests__/ReactContextPropagation-test.js | 5 ++- .../ReactHooksWithNoopRenderer-test.js | 7 ++- .../src/__tests__/ReactSuspenseList-test.js | 43 ++++++++++++++++++- .../ReactFlightDOMRelay-test.internal.js | 15 +++---- packages/react/index.stable.js | 1 - scripts/jest/TestFlags.js | 1 + 11 files changed, 89 insertions(+), 23 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 8c282e1bb730..6d169f9fbbb1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -42,7 +42,9 @@ describe('ReactDOMFizzServer', () => { } Stream = require('stream'); Suspense = React.Suspense; - SuspenseList = React.SuspenseList; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } PropTypes = require('prop-types'); @@ -656,6 +658,7 @@ describe('ReactDOMFizzServer', () => { expect(ref.current).toBe(b); }); + // @gate enableSuspenseList // @gate experimental it('shows inserted items before pending in a SuspenseList as fallbacks while hydrating', async () => { const ref = React.createRef(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9aab3bf94536..0e1e220e4fe0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -84,7 +84,9 @@ describe('ReactDOMServerPartialHydration', () => { ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); Suspense = React.Suspense; - SuspenseList = React.SuspenseList; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } IdleEventPriority = require('react-reconciler/constants').IdleEventPriority; }); @@ -1545,6 +1547,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); }); + // @gate enableSuspenseList it('shows inserted items in a SuspenseList before content is hydrated', async () => { let suspend = false; let resolve; @@ -1630,6 +1633,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(spanB); }); + // @gate enableSuspenseList it('shows is able to hydrate boundaries even if others in a list are pending', async () => { let suspend = false; let resolve; @@ -1704,7 +1708,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(container.textContent).toBe('ALoading B'); }); - // @gate experimental || www + // @gate enableSuspenseList it('clears server boundaries when SuspenseList runs out of time hydrating', async () => { let suspend = false; let resolve; @@ -1807,6 +1811,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(b); }); + // @gate enableSuspenseList it('clears server boundaries when SuspenseList suspends last row hydrating', async () => { let suspend = false; let resolve; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js index 60ab2edbdcf2..c2eb630e52ac 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js @@ -16,6 +16,7 @@ let ReactDOM; let ReactDOMServer; let ReactTestUtils; let act; +let SuspenseList; function initModules() { // Reset warning cache. @@ -26,6 +27,9 @@ function initModules() { ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); act = require('jest-react').act; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } // Make them available to the helpers. return { @@ -137,16 +141,17 @@ describe('ReactDOMServerSuspense', () => { ); }); + // @gate enableSuspenseList it('server renders a SuspenseList component and its children', async () => { const example = ( - +
A
B
-
+ ); const element = await serverRender(example); const parent = element.parentNode; diff --git a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js index 66cf344814dc..ca5c9fac5162 100644 --- a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js +++ b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js @@ -24,7 +24,9 @@ beforeEach(() => { act = require('jest-react').act; Suspense = React.Suspense; - SuspenseList = React.SuspenseList; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } getCacheForType = React.unstable_getCacheForType; @@ -197,6 +199,7 @@ test('warns in DEV if return pointer is inconsistent', async () => { }); // @gate enableCache +// @gate enableSuspenseList test('regression (#20932): return pointer is correct before entering deleted tree', async () => { // Based on a production bug. Designed to trigger a very specific // implementation path. diff --git a/packages/react-is/src/__tests__/ReactIs-test.js b/packages/react-is/src/__tests__/ReactIs-test.js index b6adc66b00b5..6a5976e97194 100644 --- a/packages/react-is/src/__tests__/ReactIs-test.js +++ b/packages/react-is/src/__tests__/ReactIs-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactIs; +let SuspenseList; describe('ReactIs', () => { beforeEach(() => { @@ -20,6 +21,10 @@ describe('ReactIs', () => { React = require('react'); ReactDOM = require('react-dom'); ReactIs = require('react-is'); + + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } }); it('should return undefined for unknown/invalid types', () => { @@ -186,10 +191,11 @@ describe('ReactIs', () => { expect(ReactIs.isSuspense(
)).toBe(false); }); + // @gate enableSuspenseList it('should identify suspense list', () => { - expect(ReactIs.isValidElementType(React.SuspenseList)).toBe(true); - expect(ReactIs.typeOf()).toBe(ReactIs.SuspenseList); - expect(ReactIs.isSuspenseList()).toBe(true); + expect(ReactIs.isValidElementType(SuspenseList)).toBe(true); + expect(ReactIs.typeOf()).toBe(ReactIs.SuspenseList); + expect(ReactIs.isSuspenseList()).toBe(true); expect(ReactIs.isSuspenseList({type: ReactIs.SuspenseList})).toBe(false); expect(ReactIs.isSuspenseList('React.SuspenseList')).toBe(false); expect(ReactIs.isSuspenseList(
)).toBe(false); diff --git a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js index f568e40ccad7..24e57002ef30 100644 --- a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js +++ b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js @@ -21,7 +21,9 @@ describe('ReactLazyContextPropagation', () => { useState = React.useState; useContext = React.useContext; Suspense = React.Suspense; - SuspenseList = React.SuspenseList; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } getCacheForType = React.unstable_getCacheForType; @@ -651,6 +653,7 @@ describe('ReactLazyContextPropagation', () => { expect(root).toMatchRenderedOutput('BBB'); }); + // @gate enableSuspenseList test('contexts are propagated through SuspenseList', async () => { // This kinda tests an implementation detail. SuspenseList has an early // bailout that doesn't use `bailoutOnAlreadyFinishedWork`. It probably diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index dae01c885fe5..811be0963915 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -34,6 +34,7 @@ let forwardRef; let memo; let act; let ContinuousEventPriority; +let SuspenseList; describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { @@ -60,6 +61,9 @@ describe('ReactHooksWithNoopRenderer', () => { Suspense = React.Suspense; ContinuousEventPriority = require('react-reconciler/constants') .ContinuousEventPriority; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } textCache = new Map(); @@ -4291,9 +4295,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); }); + // @gate enableSuspenseList it('regression: SuspenseList causes unmounts to be dropped on deletion', async () => { - const SuspenseList = React.SuspenseList; - function Row({label}) { useEffect(() => { Scheduler.unstable_yieldValue('Mount ' + label); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index e969a59c4f60..d357094f78c9 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -16,7 +16,9 @@ describe('ReactSuspenseList', () => { act = require('jest-react').act; Profiler = React.Profiler; Suspense = React.Suspense; - SuspenseList = React.SuspenseList; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } }); function Text(props) { @@ -42,6 +44,7 @@ describe('ReactSuspenseList', () => { return Component; } + // @gate enableSuspenseList it('warns if an unsupported revealOrder option is used', () => { function Foo() { return ( @@ -61,6 +64,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a upper case revealOrder option is used', () => { function Foo() { return ( @@ -80,6 +84,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a misspelled revealOrder option is used', () => { function Foo() { return ( @@ -100,6 +105,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a single element is passed to a "forwards" list', () => { function Foo({children}) { return {children}; @@ -132,6 +138,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a single fragment is passed to a "backwards" list', () => { function Foo() { return ( @@ -152,6 +159,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a nested array is passed to a "forwards" list', () => { function Foo({items}) { return ( @@ -179,6 +187,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('shows content independently by default', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -245,6 +254,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('shows content independently in legacy mode regardless of option', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -315,6 +325,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays all "together"', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -384,6 +395,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays all "together" even when nested as siblings', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -469,6 +481,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays all "together" in nested SuspenseLists', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -530,6 +543,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays all "together" in nested SuspenseLists where the inner is default', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -589,6 +603,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays all "together" during an update', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -673,6 +688,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('avoided boundaries can be coordinate with SuspenseList', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -771,6 +787,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('boundaries without fallbacks can be coordinate with SuspenseList', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -854,6 +871,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays each items in "forwards" order', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -919,6 +937,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays each items in "backwards" order', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -984,6 +1003,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays added row at the top "together" and the bottom in "forwards" order', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -1138,6 +1158,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('displays added row at the top "together" and the bottom in "backwards" order', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -1322,6 +1343,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('switches to rendering fallbacks if the tail takes long CPU time', async () => { function Foo() { return ( @@ -1390,6 +1412,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('only shows one loading state at a time for "collapsed" tail insertions', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -1459,6 +1482,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('warns if an unsupported tail option is used', () => { function Foo() { return ( @@ -1479,6 +1503,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('warns if a tail option is used with "together"', () => { function Foo() { return ( @@ -1499,6 +1524,7 @@ describe('ReactSuspenseList', () => { ]); }); + // @gate enableSuspenseList it('renders one "collapsed" fallback even if CPU time elapsed', async () => { function Foo() { return ( @@ -1571,6 +1597,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('adding to the middle does not collapse insertions (forwards)', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -1713,6 +1740,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('adding to the middle does not collapse insertions (backwards)', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -1860,6 +1888,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('adding to the middle of committed tail does not collapse insertions', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -2017,6 +2046,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('only shows no initial loading state "hidden" tail insertions', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -2080,6 +2110,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('eventually resolves a nested forwards suspense list', async () => { const B = createAsyncText('B'); @@ -2142,6 +2173,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('eventually resolves a nested forwards suspense list with a hidden tail', async () => { const B = createAsyncText('B'); @@ -2188,6 +2220,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('eventually resolves two nested forwards suspense lists with a hidden tail', async () => { const B = createAsyncText('B'); @@ -2255,6 +2288,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('can do unrelated adjacent updates', async () => { let updateAdjacent; function Adjacent() { @@ -2301,6 +2335,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('is able to re-suspend the last rows during an update with hidden', async () => { const AsyncB = createAsyncText('B'); @@ -2389,6 +2424,7 @@ describe('ReactSuspenseList', () => { expect(previousInst).toBe(setAsyncB); }); + // @gate enableSuspenseList it('is able to re-suspend the last rows during an update with hidden', async () => { const AsyncB = createAsyncText('B'); @@ -2477,6 +2513,7 @@ describe('ReactSuspenseList', () => { expect(previousInst).toBe(setAsyncB); }); + // @gate enableSuspenseList it('is able to interrupt a partially rendered tree and continue later', async () => { const AsyncA = createAsyncText('A'); @@ -2555,6 +2592,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('can resume class components when revealed together', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); @@ -2617,6 +2655,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('should be able to progressively show CPU expensive rows with two pass rendering', async () => { function TwoPass({text}) { const [pass, setPass] = React.useState(0); @@ -2687,6 +2726,7 @@ describe('ReactSuspenseList', () => { ); }); + // @gate enableSuspenseList it('should be able to progressively show rows with two pass rendering and visible', async () => { function TwoPass({text}) { const [pass, setPass] = React.useState(0); @@ -2769,6 +2809,7 @@ describe('ReactSuspenseList', () => { }); // @gate enableProfilerTimer + // @gate enableSuspenseList it('counts the actual duration when profiling a SuspenseList', async () => { // Order of parameters: id, phase, actualDuration, treeBaseDuration const onRender = jest.fn(); diff --git a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index 75ac4ef8850a..11d25a5a1196 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -13,6 +13,7 @@ let ReactDOM; let JSResourceReference; let ReactDOMFlightRelayServer; let ReactDOMFlightRelayClient; +let SuspenseList; describe('ReactFlightDOMRelay', () => { beforeEach(() => { @@ -24,6 +25,9 @@ describe('ReactFlightDOMRelay', () => { ReactDOMFlightRelayServer = require('react-server-dom-relay/server'); ReactDOMFlightRelayClient = require('react-server-dom-relay'); JSResourceReference = require('JSResourceReference'); + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.SuspenseList; + } }); function readThrough(data) { @@ -104,16 +108,9 @@ describe('ReactFlightDOMRelay', () => { expect(container.innerHTML).toEqual('Hello, Seb Smith'); }); + // @gate enableSuspenseList it('can reasonably handle different element types', () => { - const { - forwardRef, - memo, - Fragment, - StrictMode, - Profiler, - Suspense, - SuspenseList, - } = React; + const {forwardRef, memo, Fragment, StrictMode, Profiler, Suspense} = React; const Inner = memo( forwardRef((props, ref) => { diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 234ec8336a9e..b0f6f409bb0e 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -17,7 +17,6 @@ export { PureComponent, StrictMode, Suspense, - SuspenseList, cloneElement, createContext, createElement, diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js index af83baf0f3ec..e76a93638336 100644 --- a/scripts/jest/TestFlags.js +++ b/scripts/jest/TestFlags.js @@ -86,6 +86,7 @@ function getTestFlags() { // This isn't a flag, just a useful alias for tests. enableUseSyncExternalStoreShim: !__VARIANT__, + enableSuspenseList: releaseChannel === 'experimental' || www, // If there's a naming conflict between scheduler and React feature flags, the // React ones take precedence.