Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] add avoidThisFallback support #22318

Merged
merged 21 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
14 changes: 12 additions & 2 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -1497,7 +1508,6 @@ export function writeStartPendingSuspenseBoundary(
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startClientRenderedSuspenseBoundary);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export {
pushEmpty,
pushStartInstance,
pushEndInstance,
pushStartCompletedSuspenseBoundary,
pushEndCompletedSuspenseBoundary,
writeStartSegment,
writeEndSegment,
writeCompletedSegmentInstruction,
Expand Down Expand Up @@ -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
Expand All @@ -142,7 +138,6 @@ export function writeStartClientRenderedSuspenseBoundary(
return writeStartClientRenderedSuspenseBoundaryImpl(
destination,
responseState,
id,
);
}
export function writeEndCompletedSuspenseBoundary(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -220,17 +225,20 @@ 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,
responseState: ResponseState,
): boolean {
return writeChunk(destination, END);
}
export function pushEndCompletedSuspenseBoundary(
salazarm marked this conversation as resolved.
Show resolved Hide resolved
target: Array<Chunk | PrecomputedChunk>,
): void {
target.push(END);
}
export function writeEndPendingSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
Expand Down
37 changes: 29 additions & 8 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
pushTextInstance,
pushStartInstance,
pushEndInstance,
pushStartCompletedSuspenseBoundary,
pushEndCompletedSuspenseBoundary,
createSuspenseBoundaryID,
getChildFormatContext,
} from './ReactServerFormatConfig';
Expand Down Expand Up @@ -107,6 +109,7 @@ import {
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableLazyElements,
enableSuspenseAvoidThisFallback,
} from 'shared/ReactFeatureFlags';

import getComponentNameFromType from 'shared/getComponentNameFromType';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -1604,7 +1631,6 @@ function flushSegment(
writeStartClientRenderedSuspenseBoundary(
destination,
request.responseState,
boundary.id,
);

// Flush the fallback.
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export const warnAboutSpreadingKeyToJSX = false;

export const warnOnSubscriptionInsideStartTransition = false;

export const enableSuspenseAvoidThisFallback = false;

export const enableComponentStackLocations = true;

export const enableNewReconciler = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading