@samitouri / QOS-React-2 / commits / 64e70f82e9

[Fizz] add avoidThisFallback support (#22318)

salazarm committed Sep 20, 2021 at 15:44 UTC 64e70f82e99e5836307aeefbde6b58240531c1ab
15 files changed +220 -24
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+148
@@ -1484,6 +1484,154 @@ describe('ReactDOMFizzServer', () => {
1484 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
1485 });
1486
1487 + // @gate experimental && enableSuspenseAvoidThisFallback
1488 + it('should respect unstable_avoidThisFallback', async () => {
1489 + const resolved = {
1490 + 0: false,
1491 + 1: false,
1492 + };
1493 + const promiseRes = {};
1494 + const promises = {
1495 + 0: new Promise(res => {
1496 + promiseRes[0] = () => {
1497 + resolved[0] = true;
1498 + res();
1499 + };
1500 + }),
1501 + 1: new Promise(res => {
1502 + promiseRes[1] = () => {
1503 + resolved[1] = true;
1504 + res();
1505 + };
1506 + }),
1507 + };
1508 +
1509 + const InnerComponent = ({isClient, depth}) => {
1510 + if (isClient) {
1511 + // Resuspend after re-rendering on client to check that fallback shows on client
1512 + throw new Promise(() => {});
1513 + }
1514 + if (!resolved[depth]) {
1515 + throw promises[depth];
1516 + }
1517 + return (
1518 + <div>
1519 + <Text text={`resolved ${depth}`} />
1520 + </div>
1521 + );
1522 + };
1523 +
1524 + function App({isClient}) {
1525 + return (
1526 + <div>
1527 + <Text text="Non Suspense Content" />
1528 + <Suspense
1529 + fallback={
1530 + <span>
1531 + <Text text="Avoided Fallback" />
1532 + </span>
1533 + }
1534 + unstable_avoidThisFallback={true}>
1535 + <InnerComponent isClient={isClient} depth={0} />
1536 + <div>
1537 + <Suspense fallback={<Text text="Fallback" />}>
1538 + <Suspense
1539 + fallback={
1540 + <span>
1541 + <Text text="Avoided Fallback2" />
1542 + </span>
1543 + }
1544 + unstable_avoidThisFallback={true}>
1545 + <InnerComponent isClient={isClient} depth={1} />
1546 + </Suspense>
1547 + </Suspense>
1548 + </div>
1549 + </Suspense>
1550 + </div>
1551 + );
1552 + }
1553 +
1554 + await jest.runAllTimers();
1555 +
1556 + await act(async () => {
1557 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
1558 + <App isClient={false} />,
1559 + writable,
1560 + );
1561 + startWriting();
1562 + });
1563 +
1564 + // Nothing is output since root has a suspense with avoidedThisFallback that hasn't resolved
1565 + expect(getVisibleChildren(container)).toEqual(undefined);
1566 + expect(container.innerHTML).not.toContain('Avoided Fallback');
1567 +
1568 + // resolve first suspense component with avoidThisFallback
1569 + await act(async () => {
1570 + promiseRes[0]();
1571 + });
1572 +
1573 + expect(getVisibleChildren(container)).toEqual(
1574 + <div>
1575 + Non Suspense Content
1576 + <div>resolved 0</div>
1577 + <div>Fallback</div>
1578 + </div>,
1579 + );
1580 +
1581 + expect(container.innerHTML).not.toContain('Avoided Fallback2');
1582 +
1583 + await act(async () => {
1584 + promiseRes[1]();
1585 + });
1586 +
1587 + expect(getVisibleChildren(container)).toEqual(
1588 + <div>
1589 + Non Suspense Content
1590 + <div>resolved 0</div>
1591 + <div>
1592 + <div>resolved 1</div>
1593 + </div>
1594 + </div>,
1595 + );
1596 +
1597 + let root;
1598 + await act(async () => {
1599 + root = ReactDOM.hydrateRoot(container, <App isClient={false} />);
1600 + Scheduler.unstable_flushAll();
1601 + await jest.runAllTimers();
1602 + });
1603 +
1604 + // No change after hydration
1605 + expect(getVisibleChildren(container)).toEqual(
1606 + <div>
1607 + Non Suspense Content
1608 + <div>resolved 0</div>
1609 + <div>
1610 + <div>resolved 1</div>
1611 + </div>
1612 + </div>,
1613 + );
1614 +
1615 + await act(async () => {
1616 + // Trigger update by changing isClient to true
1617 + root.render(<App isClient={true} />);
1618 + Scheduler.unstable_flushAll();
1619 + await jest.runAllTimers();
1620 + });
1621 +
1622 + // Now that we've resuspended at the root we show the root fallback
1623 + expect(getVisibleChildren(container)).toEqual(
1624 + <div>
1625 + Non Suspense Content
1626 + <div style="display: none;">resolved 0</div>
1627 + <div style="display: none;">
1628 + <div>resolved 1</div>
1629 + </div>
1630 + <span>Avoided Fallback</span>
1631 + </div>,
1632 + );
1633 + });
1634 +
1635 // @gate supportsNativeUseSyncExternalStore
1636 // @gate experimental
1637 it('calls getServerSnapshot instead of getSnapshot', async () => {
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+12 -2
@@ -1480,10 +1480,21 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
1480 );
1481 const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
1482
1483 +export function pushStartCompletedSuspenseBoundary(
1484 + target: Array<Chunk | PrecomputedChunk>,
1485 +) {
1486 + target.push(startCompletedSuspenseBoundary);
1487 +}
1488 +
1489 +export function pushEndCompletedSuspenseBoundary(
1490 + target: Array<Chunk | PrecomputedChunk>,
1491 +) {
1492 + target.push(endSuspenseBoundary);
1493 +}
1494 +
1495 export function writeStartCompletedSuspenseBoundary(
1496 destination: Destination,
1497 responseState: ResponseState,
1486 - id: SuspenseBoundaryID,
1498 ): boolean {
1499 return writeChunk(destination, startCompletedSuspenseBoundary);
1500 }
@@ -1497,7 +1508,6 @@ export function writeStartPendingSuspenseBoundary(
1508 export function writeStartClientRenderedSuspenseBoundary(
1509 destination: Destination,
1510 responseState: ResponseState,
1500 - id: SuspenseBoundaryID,
1511 ): boolean {
1512 return writeChunk(destination, startClientRenderedSuspenseBoundary);
1513 }
packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+3 -8
@@ -86,6 +86,8 @@ export {
86 pushEmpty,
87 pushStartInstance,
88 pushEndInstance,
89 + pushStartCompletedSuspenseBoundary,
90 + pushEndCompletedSuspenseBoundary,
91 writeStartSegment,
92 writeEndSegment,
93 writeCompletedSegmentInstruction,
@@ -116,23 +118,17 @@ export function pushTextInstance(
118 export function writeStartCompletedSuspenseBoundary(
119 destination: Destination,
120 responseState: ResponseState,
119 - id: SuspenseBoundaryID,
121 ): boolean {
122 if (responseState.generateStaticMarkup) {
123 // A completed boundary is done and doesn't need a representation in the HTML
124 // if we're not going to be hydrating it.
125 return true;
126 }
126 - return writeStartCompletedSuspenseBoundaryImpl(
127 - destination,
128 - responseState,
129 - id,
130 - );
127 + return writeStartCompletedSuspenseBoundaryImpl(destination, responseState);
128 }
129 export function writeStartClientRenderedSuspenseBoundary(
130 destination: Destination,
131 responseState: ResponseState,
135 - id: SuspenseBoundaryID,
132 ): boolean {
133 if (responseState.generateStaticMarkup) {
134 // A client rendered boundary is done and doesn't need a representation in the HTML
@@ -142,7 +138,6 @@ export function writeStartClientRenderedSuspenseBoundary(
138 return writeStartClientRenderedSuspenseBoundaryImpl(
139 destination,
140 responseState,
145 - id,
141 );
142 }
143 export function writeEndCompletedSuspenseBoundary(
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+14 -6
@@ -204,11 +204,16 @@ export function writePlaceholder(
204 export function writeStartCompletedSuspenseBoundary(
205 destination: Destination,
206 responseState: ResponseState,
207 - id: SuspenseBoundaryID,
207 ): boolean {
209 - writeChunk(destination, SUSPENSE_COMPLETE);
210 - return writeChunk(destination, formatID(id));
208 + return writeChunk(destination, SUSPENSE_COMPLETE);
209 }
210 +
211 +export function pushStartCompletedSuspenseBoundary(
212 + target: Array<Chunk | PrecomputedChunk>,
213 +): void {
214 + target.push(SUSPENSE_COMPLETE);
215 +}
216 +
217 export function writeStartPendingSuspenseBoundary(
218 destination: Destination,
219 responseState: ResponseState,
@@ -220,10 +225,8 @@ export function writeStartPendingSuspenseBoundary(
225 export function writeStartClientRenderedSuspenseBoundary(
226 destination: Destination,
227 responseState: ResponseState,
223 - id: SuspenseBoundaryID,
228 ): boolean {
225 - writeChunk(destination, SUSPENSE_CLIENT_RENDER);
226 - return writeChunk(destination, formatID(id));
229 + return writeChunk(destination, SUSPENSE_CLIENT_RENDER);
230 }
231 export function writeEndCompletedSuspenseBoundary(
232 destination: Destination,
@@ -231,6 +234,11 @@ export function writeEndCompletedSuspenseBoundary(
234 ): boolean {
235 return writeChunk(destination, END);
236 }
237 +export function pushEndCompletedSuspenseBoundary(
238 + target: Array<Chunk | PrecomputedChunk>,
239 +): void {
240 + target.push(END);
241 +}
242 export function writeEndPendingSuspenseBoundary(
243 destination: Destination,
244 responseState: ResponseState,
packages/react-server/src/ReactFizzServer.js
+29 -8
@@ -52,6 +52,8 @@ import {
52 pushTextInstance,
53 pushStartInstance,
54 pushEndInstance,
55 + pushStartCompletedSuspenseBoundary,
56 + pushEndCompletedSuspenseBoundary,
57 createSuspenseBoundaryID,
58 getChildFormatContext,
59 } from './ReactServerFormatConfig';
@@ -107,6 +109,7 @@ import {
109 warnAboutDefaultPropsOnFunctionComponents,
110 enableScopeAPI,
111 enableLazyElements,
112 + enableSuspenseAvoidThisFallback,
113 } from 'shared/ReactFeatureFlags';
114
115 import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -520,6 +523,23 @@ function renderSuspenseBoundary(
523 popComponentStackInDEV(task);
524 }
525
526 +function renderBackupSuspenseBoundary(
527 + request: Request,
528 + task: Task,
529 + props: Object,
530 +) {
531 + pushBuiltInComponentStackInDEV(task, 'Suspense');
532 +
533 + const content = props.children;
534 + const segment = task.blockedSegment;
535 +
536 + pushStartCompletedSuspenseBoundary(segment.chunks);
537 + renderNode(request, task, content);
538 + pushEndCompletedSuspenseBoundary(segment.chunks);
539 +
540 + popComponentStackInDEV(task);
541 +}
542 +
543 function renderHostElement(
544 request: Request,
545 task: Task,
@@ -986,7 +1006,14 @@ function renderElement(
1006 }
1007 // eslint-disable-next-line-no-fallthrough
1008 case REACT_SUSPENSE_TYPE: {
989 - renderSuspenseBoundary(request, task, props);
1009 + if (
1010 + enableSuspenseAvoidThisFallback &&
1011 + props.unstable_avoidThisFallback === true
1012 + ) {
1013 + renderBackupSuspenseBoundary(request, task, props);
1014 + } else {
1015 + renderSuspenseBoundary(request, task, props);
1016 + }
1017 return;
1018 }
1019 }
@@ -1604,7 +1631,6 @@ function flushSegment(
1631 writeStartClientRenderedSuspenseBoundary(
1632 destination,
1633 request.responseState,
1607 - boundary.id,
1634 );
1635
1636 // Flush the fallback.
@@ -1658,12 +1684,7 @@ function flushSegment(
1684 return writeEndPendingSuspenseBoundary(destination, request.responseState);
1685 } else {
1686 // We can inline this boundary's content as a complete boundary.
1661 -
1662 - writeStartCompletedSuspenseBoundary(
1663 - destination,
1664 - request.responseState,
1665 - boundary.id,
1666 - );
1687 + writeStartCompletedSuspenseBoundary(destination, request.responseState);
1688
1689 const completedSegments = boundary.completedSegments;
1690 invariant(
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+4
@@ -39,6 +39,10 @@ export const pushEmpty = $$$hostConfig.pushEmpty;
39 export const pushTextInstance = $$$hostConfig.pushTextInstance;
40 export const pushStartInstance = $$$hostConfig.pushStartInstance;
41 export const pushEndInstance = $$$hostConfig.pushEndInstance;
42 +export const pushStartCompletedSuspenseBoundary =
43 + $$$hostConfig.pushStartCompletedSuspenseBoundary;
44 +export const pushEndCompletedSuspenseBoundary =
45 + $$$hostConfig.pushEndCompletedSuspenseBoundary;
46 export const writePlaceholder = $$$hostConfig.writePlaceholder;
47 export const writeStartCompletedSuspenseBoundary =
48 $$$hostConfig.writeStartCompletedSuspenseBoundary;
packages/shared/ReactFeatureFlags.js
+2
@@ -101,6 +101,8 @@ export const warnAboutSpreadingKeyToJSX = false;
101
102 export const warnOnSubscriptionInsideStartTransition = false;
103
104 +export const enableSuspenseAvoidThisFallback = false;
105 +
106 export const enableComponentStackLocations = true;
107
108 export const enableNewReconciler = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -49,6 +49,7 @@ export const disableModulePatternComponents = false;
49 export const warnUnstableRenderSubtreeIntoContainer = false;
50 export const warnAboutSpreadingKeyToJSX = false;
51 export const warnOnSubscriptionInsideStartTransition = false;
52 +export const enableSuspenseAvoidThisFallback = false;
53 export const enableComponentStackLocations = false;
54 export const enableLegacyFBSupport = false;
55 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
40 export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 +export const enableSuspenseAvoidThisFallback = false;
44 export const enableComponentStackLocations = false;
45 export const enableLegacyFBSupport = false;
46 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
40 export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 +export const enableSuspenseAvoidThisFallback = false;
44 export const enableComponentStackLocations = true;
45 export const enableLegacyFBSupport = false;
46 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -50,6 +50,7 @@ export const enableGetInspectorDataForInstanceInProduction = false;
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = false;
52 export const warnOnSubscriptionInsideStartTransition = false;
53 +export const enableSuspenseAvoidThisFallback = false;
54 export const enableStrictEffects = false;
55 export const createRootStrictEffectsByDefault = false;
56 export const enableUseRefAccessWarning = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
40 export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 +export const enableSuspenseAvoidThisFallback = false;
44 export const enableComponentStackLocations = true;
45 export const enableLegacyFBSupport = false;
46 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
40 export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 +export const enableSuspenseAvoidThisFallback = false;
44 export const enableComponentStackLocations = true;
45 export const enableLegacyFBSupport = false;
46 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
40 export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 +export const enableSuspenseAvoidThisFallback = false;
44 export const enableComponentStackLocations = true;
45 export const enableLegacyFBSupport = !__EXPERIMENTAL__;
46 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -48,6 +48,7 @@ export const enableProfilerNestedUpdateScheduledHook =
48 export const enableUpdaterTracking = __PROFILE__;
49
50 export const enableSuspenseLayoutEffectSemantics = true;
51 +export const enableSuspenseAvoidThisFallback = false;
52
53 // Logs additional User Timing API marks for use with an experimental profiling tool.
54 export const enableSchedulingProfiler =