[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 =