mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[Fizz] add avoidThisFallback support (#22318)
This commit is contained in:
@@ -1484,6 +1484,154 @@ describe('ReactDOMFizzServer', () => {
|
||||
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
|
||||
});
|
||||
|
||||
// @gate experimental && enableSuspenseAvoidThisFallback
|
||||
it('should respect unstable_avoidThisFallback', async () => {
|
||||
const resolved = {
|
||||
0: false,
|
||||
1: false,
|
||||
};
|
||||
const promiseRes = {};
|
||||
const promises = {
|
||||
0: new Promise(res => {
|
||||
promiseRes[0] = () => {
|
||||
resolved[0] = true;
|
||||
res();
|
||||
};
|
||||
}),
|
||||
1: new Promise(res => {
|
||||
promiseRes[1] = () => {
|
||||
resolved[1] = true;
|
||||
res();
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const InnerComponent = ({isClient, depth}) => {
|
||||
if (isClient) {
|
||||
// Resuspend after re-rendering on client to check that fallback shows on client
|
||||
throw new Promise(() => {});
|
||||
}
|
||||
if (!resolved[depth]) {
|
||||
throw promises[depth];
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Text text={`resolved ${depth}`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function App({isClient}) {
|
||||
return (
|
||||
<div>
|
||||
<Text text="Non Suspense Content" />
|
||||
<Suspense
|
||||
fallback={
|
||||
<span>
|
||||
<Text text="Avoided Fallback" />
|
||||
</span>
|
||||
}
|
||||
unstable_avoidThisFallback={true}>
|
||||
<InnerComponent isClient={isClient} depth={0} />
|
||||
<div>
|
||||
<Suspense fallback={<Text text="Fallback" />}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<span>
|
||||
<Text text="Avoided Fallback2" />
|
||||
</span>
|
||||
}
|
||||
unstable_avoidThisFallback={true}>
|
||||
<InnerComponent isClient={isClient} depth={1} />
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await jest.runAllTimers();
|
||||
|
||||
await act(async () => {
|
||||
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
|
||||
<App isClient={false} />,
|
||||
writable,
|
||||
);
|
||||
startWriting();
|
||||
});
|
||||
|
||||
// Nothing is output since root has a suspense with avoidedThisFallback that hasn't resolved
|
||||
expect(getVisibleChildren(container)).toEqual(undefined);
|
||||
expect(container.innerHTML).not.toContain('Avoided Fallback');
|
||||
|
||||
// resolve first suspense component with avoidThisFallback
|
||||
await act(async () => {
|
||||
promiseRes[0]();
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
Non Suspense Content
|
||||
<div>resolved 0</div>
|
||||
<div>Fallback</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(container.innerHTML).not.toContain('Avoided Fallback2');
|
||||
|
||||
await act(async () => {
|
||||
promiseRes[1]();
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
Non Suspense Content
|
||||
<div>resolved 0</div>
|
||||
<div>
|
||||
<div>resolved 1</div>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
let root;
|
||||
await act(async () => {
|
||||
root = ReactDOM.hydrateRoot(container, <App isClient={false} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
await jest.runAllTimers();
|
||||
});
|
||||
|
||||
// No change after hydration
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
Non Suspense Content
|
||||
<div>resolved 0</div>
|
||||
<div>
|
||||
<div>resolved 1</div>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
// Trigger update by changing isClient to true
|
||||
root.render(<App isClient={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
await jest.runAllTimers();
|
||||
});
|
||||
|
||||
// Now that we've resuspended at the root we show the root fallback
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
Non Suspense Content
|
||||
<div style="display: none;">resolved 0</div>
|
||||
<div style="display: none;">
|
||||
<div>resolved 1</div>
|
||||
</div>
|
||||
<span>Avoided Fallback</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate supportsNativeUseSyncExternalStore
|
||||
// @gate experimental
|
||||
it('calls getServerSnapshot instead of getSnapshot', async () => {
|
||||
|
||||
@@ -1480,10 +1480,21 @@ const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
|
||||
);
|
||||
const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
|
||||
|
||||
export function pushStartCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
) {
|
||||
target.push(startCompletedSuspenseBoundary);
|
||||
}
|
||||
|
||||
export function pushEndCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
) {
|
||||
target.push(endSuspenseBoundary);
|
||||
}
|
||||
|
||||
export function writeStartCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
return writeChunk(destination, startCompletedSuspenseBoundary);
|
||||
}
|
||||
@@ -1497,7 +1508,6 @@ export function writeStartPendingSuspenseBoundary(
|
||||
export function writeStartClientRenderedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
return writeChunk(destination, startClientRenderedSuspenseBoundary);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,8 @@ export {
|
||||
pushEmpty,
|
||||
pushStartInstance,
|
||||
pushEndInstance,
|
||||
pushStartCompletedSuspenseBoundary,
|
||||
pushEndCompletedSuspenseBoundary,
|
||||
writeStartSegment,
|
||||
writeEndSegment,
|
||||
writeCompletedSegmentInstruction,
|
||||
@@ -116,23 +118,17 @@ export function pushTextInstance(
|
||||
export function writeStartCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
if (responseState.generateStaticMarkup) {
|
||||
// A completed boundary is done and doesn't need a representation in the HTML
|
||||
// if we're not going to be hydrating it.
|
||||
return true;
|
||||
}
|
||||
return writeStartCompletedSuspenseBoundaryImpl(
|
||||
destination,
|
||||
responseState,
|
||||
id,
|
||||
);
|
||||
return writeStartCompletedSuspenseBoundaryImpl(destination, responseState);
|
||||
}
|
||||
export function writeStartClientRenderedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
if (responseState.generateStaticMarkup) {
|
||||
// A client rendered boundary is done and doesn't need a representation in the HTML
|
||||
@@ -142,7 +138,6 @@ export function writeStartClientRenderedSuspenseBoundary(
|
||||
return writeStartClientRenderedSuspenseBoundaryImpl(
|
||||
destination,
|
||||
responseState,
|
||||
id,
|
||||
);
|
||||
}
|
||||
export function writeEndCompletedSuspenseBoundary(
|
||||
|
||||
@@ -204,11 +204,16 @@ export function writePlaceholder(
|
||||
export function writeStartCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
writeChunk(destination, SUSPENSE_COMPLETE);
|
||||
return writeChunk(destination, formatID(id));
|
||||
return writeChunk(destination, SUSPENSE_COMPLETE);
|
||||
}
|
||||
|
||||
export function pushStartCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
): void {
|
||||
target.push(SUSPENSE_COMPLETE);
|
||||
}
|
||||
|
||||
export function writeStartPendingSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
@@ -220,10 +225,8 @@ export function writeStartPendingSuspenseBoundary(
|
||||
export function writeStartClientRenderedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
writeChunk(destination, SUSPENSE_CLIENT_RENDER);
|
||||
return writeChunk(destination, formatID(id));
|
||||
return writeChunk(destination, SUSPENSE_CLIENT_RENDER);
|
||||
}
|
||||
export function writeEndCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
@@ -231,6 +234,11 @@ export function writeEndCompletedSuspenseBoundary(
|
||||
): boolean {
|
||||
return writeChunk(destination, END);
|
||||
}
|
||||
export function pushEndCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
): void {
|
||||
target.push(END);
|
||||
}
|
||||
export function writeEndPendingSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
|
||||
37
packages/react-server/src/ReactFizzServer.js
vendored
37
packages/react-server/src/ReactFizzServer.js
vendored
@@ -52,6 +52,8 @@ import {
|
||||
pushTextInstance,
|
||||
pushStartInstance,
|
||||
pushEndInstance,
|
||||
pushStartCompletedSuspenseBoundary,
|
||||
pushEndCompletedSuspenseBoundary,
|
||||
createSuspenseBoundaryID,
|
||||
getChildFormatContext,
|
||||
} from './ReactServerFormatConfig';
|
||||
@@ -107,6 +109,7 @@ import {
|
||||
warnAboutDefaultPropsOnFunctionComponents,
|
||||
enableScopeAPI,
|
||||
enableLazyElements,
|
||||
enableSuspenseAvoidThisFallback,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
||||
@@ -520,6 +523,23 @@ function renderSuspenseBoundary(
|
||||
popComponentStackInDEV(task);
|
||||
}
|
||||
|
||||
function renderBackupSuspenseBoundary(
|
||||
request: Request,
|
||||
task: Task,
|
||||
props: Object,
|
||||
) {
|
||||
pushBuiltInComponentStackInDEV(task, 'Suspense');
|
||||
|
||||
const content = props.children;
|
||||
const segment = task.blockedSegment;
|
||||
|
||||
pushStartCompletedSuspenseBoundary(segment.chunks);
|
||||
renderNode(request, task, content);
|
||||
pushEndCompletedSuspenseBoundary(segment.chunks);
|
||||
|
||||
popComponentStackInDEV(task);
|
||||
}
|
||||
|
||||
function renderHostElement(
|
||||
request: Request,
|
||||
task: Task,
|
||||
@@ -986,7 +1006,14 @@ function renderElement(
|
||||
}
|
||||
// eslint-disable-next-line-no-fallthrough
|
||||
case REACT_SUSPENSE_TYPE: {
|
||||
renderSuspenseBoundary(request, task, props);
|
||||
if (
|
||||
enableSuspenseAvoidThisFallback &&
|
||||
props.unstable_avoidThisFallback === true
|
||||
) {
|
||||
renderBackupSuspenseBoundary(request, task, props);
|
||||
} else {
|
||||
renderSuspenseBoundary(request, task, props);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1604,7 +1631,6 @@ function flushSegment(
|
||||
writeStartClientRenderedSuspenseBoundary(
|
||||
destination,
|
||||
request.responseState,
|
||||
boundary.id,
|
||||
);
|
||||
|
||||
// Flush the fallback.
|
||||
@@ -1658,12 +1684,7 @@ function flushSegment(
|
||||
return writeEndPendingSuspenseBoundary(destination, request.responseState);
|
||||
} else {
|
||||
// We can inline this boundary's content as a complete boundary.
|
||||
|
||||
writeStartCompletedSuspenseBoundary(
|
||||
destination,
|
||||
request.responseState,
|
||||
boundary.id,
|
||||
);
|
||||
writeStartCompletedSuspenseBoundary(destination, request.responseState);
|
||||
|
||||
const completedSegments = boundary.completedSegments;
|
||||
invariant(
|
||||
|
||||
@@ -39,6 +39,10 @@ export const pushEmpty = $$$hostConfig.pushEmpty;
|
||||
export const pushTextInstance = $$$hostConfig.pushTextInstance;
|
||||
export const pushStartInstance = $$$hostConfig.pushStartInstance;
|
||||
export const pushEndInstance = $$$hostConfig.pushEndInstance;
|
||||
export const pushStartCompletedSuspenseBoundary =
|
||||
$$$hostConfig.pushStartCompletedSuspenseBoundary;
|
||||
export const pushEndCompletedSuspenseBoundary =
|
||||
$$$hostConfig.pushEndCompletedSuspenseBoundary;
|
||||
export const writePlaceholder = $$$hostConfig.writePlaceholder;
|
||||
export const writeStartCompletedSuspenseBoundary =
|
||||
$$$hostConfig.writeStartCompletedSuspenseBoundary;
|
||||
|
||||
@@ -101,6 +101,8 @@ export const warnAboutSpreadingKeyToJSX = false;
|
||||
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
|
||||
export const enableComponentStackLocations = true;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
|
||||
@@ -49,6 +49,7 @@ export const disableModulePatternComponents = false;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = false;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = false;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = true;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -50,6 +50,7 @@ export const enableGetInspectorDataForInstanceInProduction = false;
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableStrictEffects = false;
|
||||
export const createRootStrictEffectsByDefault = false;
|
||||
export const enableUseRefAccessWarning = false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = true;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = false;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = true;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const disableModulePatternComponents = true;
|
||||
export const warnUnstableRenderSubtreeIntoContainer = false;
|
||||
export const warnAboutSpreadingKeyToJSX = false;
|
||||
export const warnOnSubscriptionInsideStartTransition = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableComponentStackLocations = true;
|
||||
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
|
||||
export const enableFilterEmptyStringAttributesDOM = false;
|
||||
|
||||
@@ -48,6 +48,7 @@ export const enableProfilerNestedUpdateScheduledHook =
|
||||
export const enableUpdaterTracking = __PROFILE__;
|
||||
|
||||
export const enableSuspenseLayoutEffectSemantics = true;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
|
||||
// Logs additional User Timing API marks for use with an experimental profiling tool.
|
||||
export const enableSchedulingProfiler =
|
||||
|
||||
Reference in New Issue
Block a user