Skip to content

Commit 64e70f8

Browse files
authored
[Fizz] add avoidThisFallback support (#22318)
1 parent cbf6178 commit 64e70f8

15 files changed

+220
-24
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,154 @@ describe('ReactDOMFizzServer', () => {
14841484
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
14851485
});
14861486

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+
14871635
// @gate supportsNativeUseSyncExternalStore
14881636
// @gate experimental
14891637
it('calls getServerSnapshot instead of getSnapshot', async () => {

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,10 +1480,21 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
14801480
);
14811481
const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
14821482

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+
14831495
export function writeStartCompletedSuspenseBoundary(
14841496
destination: Destination,
14851497
responseState: ResponseState,
1486-
id: SuspenseBoundaryID,
14871498
): boolean {
14881499
return writeChunk(destination, startCompletedSuspenseBoundary);
14891500
}
@@ -1497,7 +1508,6 @@ export function writeStartPendingSuspenseBoundary(
14971508
export function writeStartClientRenderedSuspenseBoundary(
14981509
destination: Destination,
14991510
responseState: ResponseState,
1500-
id: SuspenseBoundaryID,
15011511
): boolean {
15021512
return writeChunk(destination, startClientRenderedSuspenseBoundary);
15031513
}

packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export {
8686
pushEmpty,
8787
pushStartInstance,
8888
pushEndInstance,
89+
pushStartCompletedSuspenseBoundary,
90+
pushEndCompletedSuspenseBoundary,
8991
writeStartSegment,
9092
writeEndSegment,
9193
writeCompletedSegmentInstruction,
@@ -116,23 +118,17 @@ export function pushTextInstance(
116118
export function writeStartCompletedSuspenseBoundary(
117119
destination: Destination,
118120
responseState: ResponseState,
119-
id: SuspenseBoundaryID,
120121
): boolean {
121122
if (responseState.generateStaticMarkup) {
122123
// A completed boundary is done and doesn't need a representation in the HTML
123124
// if we're not going to be hydrating it.
124125
return true;
125126
}
126-
return writeStartCompletedSuspenseBoundaryImpl(
127-
destination,
128-
responseState,
129-
id,
130-
);
127+
return writeStartCompletedSuspenseBoundaryImpl(destination, responseState);
131128
}
132129
export function writeStartClientRenderedSuspenseBoundary(
133130
destination: Destination,
134131
responseState: ResponseState,
135-
id: SuspenseBoundaryID,
136132
): boolean {
137133
if (responseState.generateStaticMarkup) {
138134
// A client rendered boundary is done and doesn't need a representation in the HTML
@@ -142,7 +138,6 @@ export function writeStartClientRenderedSuspenseBoundary(
142138
return writeStartClientRenderedSuspenseBoundaryImpl(
143139
destination,
144140
responseState,
145-
id,
146141
);
147142
}
148143
export function writeEndCompletedSuspenseBoundary(

packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,16 @@ export function writePlaceholder(
204204
export function writeStartCompletedSuspenseBoundary(
205205
destination: Destination,
206206
responseState: ResponseState,
207-
id: SuspenseBoundaryID,
208207
): boolean {
209-
writeChunk(destination, SUSPENSE_COMPLETE);
210-
return writeChunk(destination, formatID(id));
208+
return writeChunk(destination, SUSPENSE_COMPLETE);
211209
}
210+
211+
export function pushStartCompletedSuspenseBoundary(
212+
target: Array<Chunk | PrecomputedChunk>,
213+
): void {
214+
target.push(SUSPENSE_COMPLETE);
215+
}
216+
212217
export function writeStartPendingSuspenseBoundary(
213218
destination: Destination,
214219
responseState: ResponseState,
@@ -220,17 +225,20 @@ export function writeStartPendingSuspenseBoundary(
220225
export function writeStartClientRenderedSuspenseBoundary(
221226
destination: Destination,
222227
responseState: ResponseState,
223-
id: SuspenseBoundaryID,
224228
): boolean {
225-
writeChunk(destination, SUSPENSE_CLIENT_RENDER);
226-
return writeChunk(destination, formatID(id));
229+
return writeChunk(destination, SUSPENSE_CLIENT_RENDER);
227230
}
228231
export function writeEndCompletedSuspenseBoundary(
229232
destination: Destination,
230233
responseState: ResponseState,
231234
): boolean {
232235
return writeChunk(destination, END);
233236
}
237+
export function pushEndCompletedSuspenseBoundary(
238+
target: Array<Chunk | PrecomputedChunk>,
239+
): void {
240+
target.push(END);
241+
}
234242
export function writeEndPendingSuspenseBoundary(
235243
destination: Destination,
236244
responseState: ResponseState,

packages/react-server/src/ReactFizzServer.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ import {
5252
pushTextInstance,
5353
pushStartInstance,
5454
pushEndInstance,
55+
pushStartCompletedSuspenseBoundary,
56+
pushEndCompletedSuspenseBoundary,
5557
createSuspenseBoundaryID,
5658
getChildFormatContext,
5759
} from './ReactServerFormatConfig';
@@ -107,6 +109,7 @@ import {
107109
warnAboutDefaultPropsOnFunctionComponents,
108110
enableScopeAPI,
109111
enableLazyElements,
112+
enableSuspenseAvoidThisFallback,
110113
} from 'shared/ReactFeatureFlags';
111114

112115
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -520,6 +523,23 @@ function renderSuspenseBoundary(
520523
popComponentStackInDEV(task);
521524
}
522525

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+
523543
function renderHostElement(
524544
request: Request,
525545
task: Task,
@@ -986,7 +1006,14 @@ function renderElement(
9861006
}
9871007
// eslint-disable-next-line-no-fallthrough
9881008
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+
}
9901017
return;
9911018
}
9921019
}
@@ -1604,7 +1631,6 @@ function flushSegment(
16041631
writeStartClientRenderedSuspenseBoundary(
16051632
destination,
16061633
request.responseState,
1607-
boundary.id,
16081634
);
16091635

16101636
// Flush the fallback.
@@ -1658,12 +1684,7 @@ function flushSegment(
16581684
return writeEndPendingSuspenseBoundary(destination, request.responseState);
16591685
} else {
16601686
// 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);
16671688

16681689
const completedSegments = boundary.completedSegments;
16691690
invariant(

packages/react-server/src/forks/ReactServerFormatConfig.custom.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const pushEmpty = $$$hostConfig.pushEmpty;
3939
export const pushTextInstance = $$$hostConfig.pushTextInstance;
4040
export const pushStartInstance = $$$hostConfig.pushStartInstance;
4141
export const pushEndInstance = $$$hostConfig.pushEndInstance;
42+
export const pushStartCompletedSuspenseBoundary =
43+
$$$hostConfig.pushStartCompletedSuspenseBoundary;
44+
export const pushEndCompletedSuspenseBoundary =
45+
$$$hostConfig.pushEndCompletedSuspenseBoundary;
4246
export const writePlaceholder = $$$hostConfig.writePlaceholder;
4347
export const writeStartCompletedSuspenseBoundary =
4448
$$$hostConfig.writeStartCompletedSuspenseBoundary;

packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export const warnAboutSpreadingKeyToJSX = false;
101101

102102
export const warnOnSubscriptionInsideStartTransition = false;
103103

104+
export const enableSuspenseAvoidThisFallback = false;
105+
104106
export const enableComponentStackLocations = true;
105107

106108
export const enableNewReconciler = false;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const disableModulePatternComponents = false;
4949
export const warnUnstableRenderSubtreeIntoContainer = false;
5050
export const warnAboutSpreadingKeyToJSX = false;
5151
export const warnOnSubscriptionInsideStartTransition = false;
52+
export const enableSuspenseAvoidThisFallback = false;
5253
export const enableComponentStackLocations = false;
5354
export const enableLegacyFBSupport = false;
5455
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
4040
export const warnUnstableRenderSubtreeIntoContainer = false;
4141
export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
43+
export const enableSuspenseAvoidThisFallback = false;
4344
export const enableComponentStackLocations = false;
4445
export const enableLegacyFBSupport = false;
4546
export const enableFilterEmptyStringAttributesDOM = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
4040
export const warnUnstableRenderSubtreeIntoContainer = false;
4141
export const warnAboutSpreadingKeyToJSX = false;
4242
export const warnOnSubscriptionInsideStartTransition = false;
43+
export const enableSuspenseAvoidThisFallback = false;
4344
export const enableComponentStackLocations = true;
4445
export const enableLegacyFBSupport = false;
4546
export const enableFilterEmptyStringAttributesDOM = false;

0 commit comments

Comments
 (0)