Update on "[compiler] Override type provider for hook-like names"

If an imported name is hook-like but a type provider provides a non-hook type, we now override the returned value and treat it as a generic custom hook. This is meant as an extra check to prevent miscompilation.

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2024-09-05 09:17:25 -07:00
40 changed files with 1942 additions and 519 deletions
@@ -148,7 +148,15 @@ describe('ReactCache', () => {
error = e;
}
expect(error.message).toMatch('Failed to load: Hi');
assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
assertLog([
'Promise rejected [Hi]',
'Error! [Hi]',
'Error! [Hi]',
...(gate('enableSiblingPrerendering')
? ['Error! [Hi]', 'Error! [Hi]']
: []),
]);
// Should throw again on a subsequent read
root.render(<App />);
@@ -191,6 +199,7 @@ describe('ReactCache', () => {
}
});
// @gate enableSiblingPrerendering
it('evicts least recently used values', async () => {
ReactCache.unstable_setGlobalCacheLimit(3);
@@ -206,15 +215,13 @@ describe('ReactCache', () => {
await waitForAll(['Suspend! [1]', 'Loading...']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [1]']);
await waitForAll([1, 'Suspend! [2]']);
await waitForAll([1, 'Suspend! [2]', 1, 'Suspend! [2]', 'Suspend! [3]']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]']);
assertLog(['Promise resolved [2]', 'Promise resolved [3]']);
await waitForAll([1, 2, 3]);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [3]', 1, 2, 3]);
expect(root).toMatchRenderedOutput('123');
// Render 1, 4, 5
@@ -234,6 +241,9 @@ describe('ReactCache', () => {
1,
4,
'Suspend! [5]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
@@ -267,6 +277,9 @@ describe('ReactCache', () => {
1,
2,
'Suspend! [3]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
+13
View File
@@ -284,6 +284,19 @@ export function createHOCFilter(isEnabled: boolean = true) {
};
}
export function createEnvironmentNameFilter(
env: string,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
return {
type: Types.ComponentFilterEnvironmentName,
isEnabled,
isValid: true,
value: env,
};
}
export function createElementTypeFilter(
elementType: ElementType,
isEnabled: boolean = true,
+123 -65
View File
@@ -220,6 +220,7 @@ export default class Agent extends EventEmitter<{
this.updateConsolePatchSettings,
);
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
bridge.addListener('getEnvironmentNames', this.getEnvironmentNames);
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
// We do this because React Native embeds the React DevTools backend,
@@ -341,84 +342,123 @@ export default class Agent extends EventEmitter<{
}
getIDForHostInstance(target: HostInstance): number | null {
let bestMatch: null | HostInstance = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null = renderer.getNearestMountedHostInstance(target);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (
bestMatch === null ||
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const match = renderer.getElementIDForHostInstance(target);
if (match != null) {
return match;
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
return bestRenderer.getElementIDForHostInstance(bestMatch, true);
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
return bestRenderer.getElementIDForHostInstance(bestMatch);
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
}
return null;
}
getComponentNameForHostInstance(target: HostInstance): string | null {
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
let bestMatch: null | HostInstance = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode = renderer.getNearestMountedHostInstance(target);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (
bestMatch === null ||
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const id = renderer.getElementIDForHostInstance(target);
if (id) {
return renderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch, true);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
}
return null;
}
getBackendVersion: () => void = () => {
@@ -814,6 +854,24 @@ export default class Agent extends EventEmitter<{
}
};
getEnvironmentNames: () => void = () => {
let accumulatedNames = null;
for (const rendererID in this._rendererInterfaces) {
const renderer = this._rendererInterfaces[+rendererID];
const names = renderer.getEnvironmentNames();
if (accumulatedNames === null) {
accumulatedNames = names;
} else {
for (let i = 0; i < names.length; i++) {
if (accumulatedNames.indexOf(names[i]) === -1) {
accumulatedNames.push(names[i]);
}
}
}
}
this._bridge.send('environmentNames', accumulatedNames || []);
};
onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
this.emit('traceUpdates', nodes);
};
+1 -11
View File
@@ -135,17 +135,7 @@ export function registerRenderer(
renderer: ReactRenderer,
onErrorOrWarning?: OnErrorOrWarning,
): void {
const {
currentDispatcherRef,
getCurrentFiber,
findFiberByHostInstance,
version,
} = renderer;
// Ignore React v15 and older because they don't expose a component stack anyway.
if (typeof findFiberByHostInstance !== 'function') {
return;
}
const {currentDispatcherRef, getCurrentFiber, version} = renderer;
// currentDispatcherRef gets injected for v16.8+ to support hooks inspection.
// getCurrentFiber gets injected for v16.9+.
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -73,7 +73,12 @@ export function initBackend(
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
if (rendererInterface == null) {
if (typeof renderer.findFiberByHostInstance === 'function') {
if (
// v16-19
typeof renderer.findFiberByHostInstance === 'function' ||
// v16.8+
renderer.currentDispatcherRef != null
) {
// react-reconciler v16+
rendererInterface = attach(hook, id, renderer, global);
} else if (renderer.ComponentTree) {
@@ -145,15 +145,13 @@ export function attach(
let getElementIDForHostInstance: GetElementIDForHostInstance =
((null: any): GetElementIDForHostInstance);
let findHostInstanceForInternalID: (id: number) => ?HostInstance;
let getNearestMountedHostInstance = (
node: HostInstance,
): null | HostInstance => {
let getNearestMountedDOMNode = (node: Element): null | Element => {
// Not implemented.
return null;
};
if (renderer.ComponentTree) {
getElementIDForHostInstance = (node, findNearestUnfilteredAncestor) => {
getElementIDForHostInstance = node => {
const internalInstance =
renderer.ComponentTree.getClosestInstanceFromNode(node);
return internalInstanceToIDMap.get(internalInstance) || null;
@@ -162,9 +160,7 @@ export function attach(
const internalInstance = idToInternalInstanceMap.get(id);
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
};
getNearestMountedHostInstance = (
node: HostInstance,
): null | HostInstance => {
getNearestMountedDOMNode = (node: Element): null | Element => {
const internalInstance =
renderer.ComponentTree.getClosestInstanceFromNode(node);
if (internalInstance != null) {
@@ -173,7 +169,7 @@ export function attach(
return null;
};
} else if (renderer.Mount.getID && renderer.Mount.getNode) {
getElementIDForHostInstance = (node, findNearestUnfilteredAncestor) => {
getElementIDForHostInstance = node => {
// Not implemented.
return null;
};
@@ -1078,6 +1074,11 @@ export function attach(
// Not implemented.
}
function getEnvironmentNames(): Array<string> {
// No RSC support.
return [];
}
function setTraceUpdatesEnabled(enabled: boolean) {
// Not implemented.
}
@@ -1121,7 +1122,7 @@ export function attach(
flushInitialOperations,
getBestMatchForTrackedPath,
getDisplayNameForElementID,
getNearestMountedHostInstance,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getInstanceAndStyle,
findHostInstancesForElementID: (id: number) => {
@@ -1152,5 +1153,6 @@ export function attach(
storeAsGlobal,
unpatchConsoleForStrictMode,
updateComponentFilters,
getEnvironmentNames,
};
}
+4 -5
View File
@@ -90,7 +90,6 @@ export type GetDisplayNameForElementID = (id: number) => string | null;
export type GetElementIDForHostInstance = (
component: HostInstance,
findNearestUnfilteredAncestor?: boolean,
) => number | null;
export type FindHostInstancesForElementID = (
id: number,
@@ -106,10 +105,11 @@ export type Lane = number;
export type Lanes = number;
export type ReactRenderer = {
findFiberByHostInstance: (hostInstance: HostInstance) => Fiber | null,
version: string,
rendererPackageName: string,
bundleType: BundleType,
// 16.0+ - To be removed in future versions.
findFiberByHostInstance?: (hostInstance: HostInstance) => Fiber | null,
// 16.9+
overrideHookState?: ?(
fiber: Object,
@@ -358,9 +358,7 @@ export type RendererInterface = {
findHostInstancesForElementID: FindHostInstancesForElementID,
flushInitialOperations: () => void,
getBestMatchForTrackedPath: () => PathMatch | null,
getNearestMountedHostInstance: (
component: HostInstance,
) => HostInstance | null,
getNearestMountedDOMNode: (component: Element) => Element | null,
getElementIDForHostInstance: GetElementIDForHostInstance,
getDisplayNameForElementID: GetDisplayNameForElementID,
getInstanceAndStyle(id: number): InstanceAndStyle,
@@ -416,6 +414,7 @@ export type RendererInterface = {
) => void,
unpatchConsoleForStrictMode: () => void,
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
getEnvironmentNames: () => Array<string>,
// Timeline profiler interface
+2
View File
@@ -189,6 +189,7 @@ export type BackendEvents = {
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],
environmentNames: [Array<string>],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
reloadAppForProfiling: [],
@@ -237,6 +238,7 @@ type FrontendEvents = {
stopProfiling: [],
storeAsGlobal: [StoreAsGlobalParams],
updateComponentFilters: [Array<ComponentFilter>],
getEnvironmentNames: [],
updateConsolePatchSettings: [ConsolePatchSettings],
viewAttributeSource: [ViewAttributeSourceParams],
viewElementSource: [ElementAndRendererID],
@@ -15,6 +15,7 @@ import {
useMemo,
useRef,
useState,
use,
} from 'react';
import {
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
@@ -31,6 +32,7 @@ import {
ComponentFilterElementType,
ComponentFilterHOC,
ComponentFilterLocation,
ComponentFilterEnvironmentName,
ElementTypeClass,
ElementTypeContext,
ElementTypeFunction,
@@ -52,11 +54,16 @@ import type {
ElementType,
ElementTypeComponentFilter,
RegExpComponentFilter,
EnvironmentNameComponentFilter,
} from 'react-devtools-shared/src/frontend/types';
const vscodeFilepath = 'vscode://file/{path}:{line}';
export default function ComponentsSettings(_: {}): React.Node {
export default function ComponentsSettings({
environmentNames,
}: {
environmentNames: Promise<Array<string>>,
}): React.Node {
const store = useContext(StoreContext);
const {parseHookNames, setParseHookNames} = useContext(SettingsContext);
@@ -101,6 +108,30 @@ export default function ComponentsSettings(_: {}): React.Node {
Array<ComponentFilter>,
>(() => [...store.componentFilters]);
const usedEnvironmentNames = use(environmentNames);
const resolvedEnvironmentNames = useMemo(() => {
const set = new Set(usedEnvironmentNames);
// If there are other filters already specified but are not currently
// on the page, we still allow them as options.
for (let i = 0; i < componentFilters.length; i++) {
const filter = componentFilters[i];
if (filter.type === ComponentFilterEnvironmentName) {
set.add(filter.value);
}
}
// Client is special and is always available as a default.
if (set.size > 0) {
// Only show any options at all if there's any other option already
// used by a filter or if any environments are used by the page.
// Note that "Client" can have been added above which would mean
// that we should show it as an option regardless if it's the only
// option.
set.add('Client');
}
return Array.from(set).sort();
}, [usedEnvironmentNames, componentFilters]);
const addFilter = useCallback(() => {
setComponentFilters(prevComponentFilters => {
return [
@@ -146,6 +177,13 @@ export default function ComponentsSettings(_: {}): React.Node {
isEnabled: componentFilter.isEnabled,
isValid: true,
};
} else if (type === ComponentFilterEnvironmentName) {
cloned[index] = {
type: ComponentFilterEnvironmentName,
isEnabled: componentFilter.isEnabled,
isValid: true,
value: 'Client',
};
}
}
return cloned;
@@ -210,6 +248,29 @@ export default function ComponentsSettings(_: {}): React.Node {
[],
);
const updateFilterValueEnvironmentName = useCallback(
(componentFilter: ComponentFilter, value: string) => {
if (componentFilter.type !== ComponentFilterEnvironmentName) {
throw Error('Invalid value for environment name filter');
}
setComponentFilters(prevComponentFilters => {
const cloned: Array<ComponentFilter> = [...prevComponentFilters];
if (componentFilter.type === ComponentFilterEnvironmentName) {
const index = prevComponentFilters.indexOf(componentFilter);
if (index >= 0) {
cloned[index] = {
...componentFilter,
value,
};
}
}
return cloned;
});
},
[],
);
const removeFilter = useCallback((index: number) => {
setComponentFilters(prevComponentFilters => {
const cloned: Array<ComponentFilter> = [...prevComponentFilters];
@@ -246,6 +307,11 @@ export default function ComponentsSettings(_: {}): React.Node {
...((cloned[index]: any): BooleanComponentFilter),
isEnabled,
};
} else if (componentFilter.type === ComponentFilterEnvironmentName) {
cloned[index] = {
...((cloned[index]: any): EnvironmentNameComponentFilter),
isEnabled,
};
}
}
return cloned;
@@ -380,10 +446,16 @@ export default function ComponentsSettings(_: {}): React.Node {
<option value={ComponentFilterDisplayName}>name</option>
<option value={ComponentFilterElementType}>type</option>
<option value={ComponentFilterHOC}>hoc</option>
{resolvedEnvironmentNames.length > 0 && (
<option value={ComponentFilterEnvironmentName}>
environment
</option>
)}
</select>
</td>
<td className={styles.TableCell}>
{componentFilter.type === ComponentFilterElementType &&
{(componentFilter.type === ComponentFilterElementType ||
componentFilter.type === ComponentFilterEnvironmentName) &&
'equals'}
{(componentFilter.type === ComponentFilterLocation ||
componentFilter.type === ComponentFilterDisplayName) &&
@@ -428,6 +500,23 @@ export default function ComponentsSettings(_: {}): React.Node {
value={componentFilter.value}
/>
)}
{componentFilter.type === ComponentFilterEnvironmentName && (
<select
className={styles.Select}
value={componentFilter.value}
onChange={({currentTarget}) =>
updateFilterValueEnvironmentName(
componentFilter,
currentTarget.value,
)
}>
{resolvedEnvironmentNames.map(name => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
)}
</td>
<td className={styles.TableCell}>
<Button
@@ -58,7 +58,8 @@ export default function SettingsModal(_: {}): React.Node {
}
function SettingsModalImpl(_: {}) {
const {setIsModalShowing} = useContext(SettingsModalContext);
const {setIsModalShowing, environmentNames} =
useContext(SettingsModalContext);
const dismissModal = useCallback(
() => setIsModalShowing(false),
[setIsModalShowing],
@@ -81,7 +82,7 @@ function SettingsModalImpl(_: {}) {
let view = null;
switch (selectedTabID) {
case 'components':
view = <ComponentsSettings />;
view = <ComponentsSettings environmentNames={environmentNames} />;
break;
// $FlowFixMe[incompatible-type] is this missing in TabID?
case 'debugging':
@@ -10,7 +10,16 @@
import type {ReactContext} from 'shared/ReactTypes';
import * as React from 'react';
import {createContext, useMemo, useState} from 'react';
import {
createContext,
useContext,
useCallback,
useState,
startTransition,
} from 'react';
import {BridgeContext} from '../context';
import type {FrontendBridge} from '../../../bridge';
export type DisplayDensity = 'comfortable' | 'compact';
export type Theme = 'auto' | 'light' | 'dark';
@@ -18,7 +27,7 @@ export type Theme = 'auto' | 'light' | 'dark';
type Context = {
isModalShowing: boolean,
setIsModalShowing: (value: boolean) => void,
...
environmentNames: null | Promise<Array<string>>,
};
const SettingsModalContext: ReactContext<Context> = createContext<Context>(
@@ -26,20 +35,42 @@ const SettingsModalContext: ReactContext<Context> = createContext<Context>(
);
SettingsModalContext.displayName = 'SettingsModalContext';
function fetchEnvironmentNames(bridge: FrontendBridge): Promise<Array<string>> {
return new Promise(resolve => {
function onEnvironmentNames(names: Array<string>) {
bridge.removeListener('environmentNames', onEnvironmentNames);
resolve(names);
}
bridge.addListener('environmentNames', onEnvironmentNames);
bridge.send('getEnvironmentNames');
});
}
function SettingsModalContextController({
children,
}: {
children: React$Node,
}): React.Node {
const [isModalShowing, setIsModalShowing] = useState<boolean>(false);
const bridge = useContext(BridgeContext);
const value = useMemo(
() => ({isModalShowing, setIsModalShowing}),
[isModalShowing, setIsModalShowing],
);
const setIsModalShowing: boolean => void = useCallback((value: boolean) => {
startTransition(() => {
setContext({
isModalShowing: value,
setIsModalShowing,
environmentNames: value ? fetchEnvironmentNames(bridge) : null,
});
});
});
const [currentContext, setContext] = useState<Context>({
isModalShowing: false,
setIsModalShowing,
environmentNames: null,
});
return (
<SettingsModalContext.Provider value={value}>
<SettingsModalContext.Provider value={currentContext}>
{children}
</SettingsModalContext.Provider>
);
+11 -2
View File
@@ -76,8 +76,9 @@ export const ComponentFilterElementType = 1;
export const ComponentFilterDisplayName = 2;
export const ComponentFilterLocation = 3;
export const ComponentFilterHOC = 4;
export const ComponentFilterEnvironmentName = 5;
export type ComponentFilterType = 1 | 2 | 3 | 4;
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5;
// Hide all elements of types in this Set.
// We hide host components only by default.
@@ -102,10 +103,18 @@ export type BooleanComponentFilter = {
type: 4,
};
export type EnvironmentNameComponentFilter = {
isEnabled: boolean,
isValid: boolean,
type: 5,
value: string,
};
export type ComponentFilter =
| BooleanComponentFilter
| ElementTypeComponentFilter
| RegExpComponentFilter;
| RegExpComponentFilter
| EnvironmentNameComponentFilter;
export type HookName = string | null;
// Map of hook source ("<filename>:<line-number>:<column-number>") to name.
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog([]);
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
await act(async () => {
resolvePromise();
+54 -7
View File
@@ -699,7 +699,15 @@ describe('ReactDOMForm', () => {
// This should suspend because form actions are implicitly wrapped
// in startTransition.
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Updated]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Initial');
await act(() => resolveText('Updated'));
@@ -736,7 +744,15 @@ describe('ReactDOMForm', () => {
// Update
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Count: 1]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 1]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Count: 0');
await act(() => resolveText('Count: 1'));
@@ -745,7 +761,15 @@ describe('ReactDOMForm', () => {
// Update again
await submit(formRef.current);
assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
assertLog([
'Pending...',
'Suspend! [Count: 2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Count: 1');
await act(() => resolveText('Count: 2'));
@@ -789,7 +813,14 @@ describe('ReactDOMForm', () => {
assertLog(['Async action started', 'Pending...']);
await act(() => resolveText('Wait'));
assertLog(['Suspend! [Updated]', 'Loading...']);
assertLog([
'Suspend! [Updated]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Pending...Initial');
await act(() => resolveText('Updated'));
@@ -1475,7 +1506,15 @@ describe('ReactDOMForm', () => {
// Now dispatch inside of a transition. This one does not trigger a
// loading state.
await act(() => startTransition(() => dispatch()));
assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
assertLog([
'Count: 1',
'Suspend! [Count: 2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
expect(container.textContent).toBe('Count: 1');
await act(() => resolveText('Count: 2'));
@@ -1495,7 +1534,11 @@ describe('ReactDOMForm', () => {
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<App />));
assertLog(['Suspend! [Count: 0]']);
assertLog([
'Suspend! [Count: 0]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 0]'] : []),
]);
await act(() => resolveText('Count: 0'));
assertLog(['Count: 0']);
@@ -1508,7 +1551,11 @@ describe('ReactDOMForm', () => {
{withoutStack: true},
],
]);
assertLog(['Suspend! [Count: 1]']);
assertLog([
'Suspend! [Count: 1]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 1]'] : []),
]);
expect(container.textContent).toBe('Count: 0');
});
+20 -8
View File
@@ -404,10 +404,16 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
@@ -503,10 +509,16 @@ export function resetWorkInProgress(
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
if (enableProfilerTimer) {
// Note: We don't reset the actualTime counts. It's useful to accumulate
+12
View File
@@ -637,6 +637,18 @@ function finishRenderingHooks<Props, SecondArg>(
): void {
if (__DEV__) {
workInProgress._debugHookTypes = hookTypesDev;
// Stash the thenable state for use by DevTools.
if (workInProgress.dependencies === null) {
if (thenableState !== null) {
workInProgress.dependencies = {
lanes: NoLanes,
firstContext: null,
_debugThenableState: thenableState,
};
}
} else {
workInProgress.dependencies._debugThenableState = thenableState;
}
}
// We can assume the previous dispatcher is always this one, since we set it
+50
View File
@@ -229,28 +229,49 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const warmLanes = root.warmLanes;
// Do not work on any idle work until all the non-idle work has finished,
// even if the work is suspended.
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
// First check for fresh updates.
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
// No fresh updates. Check if suspended work has been pinged.
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
} else {
// The only remaining work is Idle.
// TODO: Idle isn't really used anywhere, and the thinking around
// speculative rendering has evolved since this was implemented. Consider
// removing until we've thought about this again.
// First check for fresh updates.
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
// No fresh updates. Check if suspended work has been pinged.
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
const lanesToPrewarm = pendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
@@ -335,6 +356,21 @@ export function getNextLanesToFlushSync(
return NoLanes;
}
export function checkIfRootIsPrerendering(
root: FiberRoot,
renderLanes: Lanes,
): boolean {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
// Remove lanes that are suspended (but not pinged)
const unblockedLanes = pendingLanes & ~(suspendedLanes & ~pingedLanes);
// If there are no unsuspended or pinged lanes, that implies that we're
// performing a prerender.
return (unblockedLanes & renderLanes) === 0;
}
export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
let entangledLanes = renderLanes;
@@ -670,6 +706,7 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;
}
}
@@ -677,10 +714,19 @@ export function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
) {
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;
if (!didSkipSuspendedSiblings) {
// Mark these lanes as warm so we know there's nothing else to work on.
root.warmLanes |= suspendedLanes;
} else {
// Render unwound without attempting all the siblings. Do no mark the lanes
// as warm. This will cause a prewarm render to be scheduled.
}
// The suspended lanes are no longer CPU-bound. Clear their expiration times.
const expirationTimes = root.expirationTimes;
let lanes = suspendedLanes;
@@ -700,6 +746,9 @@ export function markRootSuspended(
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
// The data that just resolved could have unblocked additional children, which
// will also need to be prewarmed if something suspends again.
root.warmLanes &= ~pingedLanes;
}
export function markRootFinished(
@@ -714,6 +763,7 @@ export function markRootFinished(
// Let's try everything again
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;
root.expiredLanes &= remainingLanes;
+20 -8
View File
@@ -825,10 +825,16 @@ function readContextForConsumer_withSelect<C>(
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
@@ -869,10 +875,16 @@ function readContextForConsumer<C>(
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
+1
View File
@@ -75,6 +75,7 @@ function FiberRootNode(
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.warmLanes = NoLanes;
this.expiredLanes = NoLanes;
this.finishedLanes = NoLanes;
this.errorRecoveryDisabledLanes = NoLanes;
+105 -22
View File
@@ -43,6 +43,7 @@ import {
disableLegacyMode,
disableDefaultPropsExceptForClasses,
disableStringRefs,
enableSiblingPrerendering,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
@@ -160,6 +161,7 @@ import {
SyncUpdateLanes,
UpdateLanes,
claimNextTransitionLane,
checkIfRootIsPrerendering,
} from './ReactFiberLane';
import {
DiscreteEventPriority,
@@ -328,6 +330,15 @@ const SuspendedOnHydration: SuspendedReason = 8;
let workInProgressSuspendedReason: SuspendedReason = NotSuspended;
let workInProgressThrownValue: mixed = null;
// Tracks whether any siblings were skipped during the unwind phase after
// something suspends. Used to determine whether to schedule another render
// to prewarm the skipped siblings.
let workInProgressRootDidSkipSuspendedSiblings: boolean = false;
// Whether the work-in-progress render is the result of a prewarm/prerender.
// This tells us whether or not we should render the siblings after
// something suspends.
let workInProgressRootIsPrerendering: boolean = false;
// Whether a ping listener was attached during this render. This is slightly
// different that whether something suspended, because we don't add multiple
// listeners to a promise we've already seen (per root and lane).
@@ -730,6 +741,7 @@ export function scheduleUpdateOnFiber(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
}
@@ -796,6 +808,7 @@ export function scheduleUpdateOnFiber(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
}
}
@@ -908,7 +921,12 @@ export function performConcurrentWorkOnRoot(
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes, NoLane);
markRootSuspended(
root,
lanes,
NoLane,
workInProgressRootDidSkipSuspendedSiblings,
);
} else {
// The render completed.
@@ -964,7 +982,12 @@ export function performConcurrentWorkOnRoot(
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes, NoLane);
markRootSuspended(
root,
lanes,
NoLane,
workInProgressRootDidSkipSuspendedSiblings,
);
break;
}
@@ -1087,7 +1110,12 @@ function finishConcurrentRender(
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
markRootSuspended(root, lanes, workInProgressDeferredLane);
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
return;
}
// Commit the placeholder.
@@ -1131,7 +1159,12 @@ function finishConcurrentRender(
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
markRootSuspended(root, lanes, workInProgressDeferredLane);
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
const nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
@@ -1155,6 +1188,7 @@ function finishConcurrentRender(
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
),
msUntilTimeout,
);
@@ -1169,6 +1203,7 @@ function finishConcurrentRender(
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
}
}
@@ -1181,6 +1216,7 @@ function commitRootWhenReady(
didIncludeRenderPhaseUpdate: boolean,
lanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
) {
// TODO: Combine retry throttling with Suspensey commits. Right now they run
// one after the other.
@@ -1220,7 +1256,7 @@ function commitRootWhenReady(
didIncludeRenderPhaseUpdate,
),
);
markRootSuspended(root, lanes, spawnedLane);
markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
return;
}
}
@@ -1332,6 +1368,7 @@ function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
) {
// When suspending, we should always exclude lanes that were pinged or (more
// rarely, since we try to avoid it) updated during the render phase.
@@ -1340,7 +1377,12 @@ function markRootSuspended(
suspendedLanes,
workInProgressRootInterleavedUpdatedLanes,
);
_markRootSuspended(root, suspendedLanes, spawnedLane);
_markRootSuspended(
root,
suspendedLanes,
spawnedLane,
didSkipSuspendedSiblings,
);
}
// This is the entry point for synchronous tasks that don't go
@@ -1392,7 +1434,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes, NoLane);
markRootSuspended(root, lanes, NoLane, false);
ensureRootIsScheduled(root);
return null;
}
@@ -1401,7 +1443,12 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes, workInProgressDeferredLane);
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
ensureRootIsScheduled(root);
return null;
}
@@ -1635,6 +1682,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
workInProgressRootRenderLanes = lanes;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
workInProgressRootDidSkipSuspendedSiblings = false;
workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootSkippedLanes = NoLanes;
@@ -1700,6 +1749,10 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
// deprecate the old API in favor of `use`.
thrownValue = getSuspendedThenable();
workInProgressSuspendedReason =
// TODO: Suspending the work loop during the render phase is
// currently not compatible with sibling prerendering. We will add
// this optimization back in a later step.
!enableSiblingPrerendering &&
shouldRemainOnPreviousScreen() &&
// Check if there are other pending updates that might possibly unblock this
// component from suspending. This mirrors the check in
@@ -1935,6 +1988,7 @@ export function renderDidSuspendDelayIfPossible(): void {
workInProgressRoot,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
workInProgressRootDidSkipSuspendedSiblings,
);
}
}
@@ -2584,7 +2638,30 @@ function throwAndUnwindWorkLoop(
if (unitOfWork.flags & Incomplete) {
// Unwind the stack until we reach the nearest boundary.
unwindUnitOfWork(unitOfWork);
let skipSiblings;
if (!enableSiblingPrerendering) {
skipSiblings = true;
} else {
if (
// The current algorithm for both hydration and error handling assumes
// that the tree is rendered sequentially. So we always skip the siblings.
getIsHydrating() ||
workInProgressSuspendedReason === SuspendedOnError
) {
skipSiblings = true;
// We intentionally don't set workInProgressRootDidSkipSuspendedSiblings,
// because we don't want to trigger another prerender attempt.
} else if (!workInProgressRootIsPrerendering) {
// This is not a prerender. Skip the siblings during this render. A
// separate prerender will be scheduled for later.
skipSiblings = true;
workInProgressRootDidSkipSuspendedSiblings = true;
} else {
// This is a prerender. Don't skip the siblings.
skipSiblings = false;
}
}
unwindUnitOfWork(unitOfWork, skipSiblings);
} else {
// Although the fiber suspended, we're intentionally going to commit it in
// an inconsistent state. We can do this safely in cases where we know the
@@ -2620,15 +2697,16 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
if (__DEV__) {
if ((completedWork.flags & Incomplete) !== NoFlags) {
// NOTE: If we re-enable sibling prerendering in some cases, this branch
// is where we would switch to the unwinding path.
console.error(
'Internal React error: Expected this fiber to be complete, but ' +
"it isn't. It should have been unwound. This is a bug in React.",
);
}
if ((completedWork.flags & Incomplete) !== NoFlags) {
// This fiber did not complete, because one of its children did not
// complete. Switch to unwinding the stack instead of completing it.
//
// The reason "unwind" and "complete" is interleaved is because when
// something suspends, we continue rendering the siblings even though
// they will be replaced by a fallback.
const skipSiblings = workInProgressRootDidSkipSuspendedSiblings;
unwindUnitOfWork(completedWork, skipSiblings);
return;
}
// The current, flushed, state of this fiber is the alternate. Ideally
@@ -2692,7 +2770,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
}
}
function unwindUnitOfWork(unitOfWork: Fiber): void {
function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void {
let incompleteWork: Fiber = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
@@ -2749,9 +2827,14 @@ function unwindUnitOfWork(unitOfWork: Fiber): void {
returnFiber.deletions = null;
}
// NOTE: If we re-enable sibling prerendering in some cases, here we
// would switch to the normal completion path: check if a sibling
// exists, and if so, begin work on it.
if (!skipSiblings) {
const siblingFiber = incompleteWork.sibling;
if (siblingFiber !== null) {
// This branch will return us to the normal work loop.
workInProgress = siblingFiber;
return;
}
}
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null
+3 -1
View File
@@ -37,6 +37,7 @@ import type {
} from './ReactFiberTracingMarkerComponent';
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates';
import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack';
import type {ThenableState} from './ReactFiberThenable';
// Unwind Circular: moved from ReactFiberHooks.old
export type HookType =
@@ -81,7 +82,7 @@ export type Dependencies = {
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null,
...
_debugThenableState?: null | ThenableState, // DEV-only
};
export type MemoCache = {
@@ -255,6 +256,7 @@ type BaseFiberRootProperties = {
pendingLanes: Lanes,
suspendedLanes: Lanes,
pingedLanes: Lanes,
warmLanes: Lanes,
expiredLanes: Lanes,
errorRecoveryDisabledLanes: Lanes,
shellSuspendCounter: number,
@@ -215,7 +215,15 @@ describe('Activity Suspense', () => {
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog([
'Open',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Open', 'Suspend! [Async]', 'Loading...']
: []),
]);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -276,7 +284,15 @@ describe('Activity Suspense', () => {
);
});
});
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog([
'Open',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Open', 'Suspend! [Async]', 'Loading...']
: []),
]);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(
@@ -349,7 +349,14 @@ describe('act warnings', () => {
root.render(<App showMore={true} />);
});
});
assertLog(['Suspend! [Async]', 'Loading...']);
assertLog([
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput('(empty)');
// This is a ping, not a retry, because no fallback is showing.
@@ -297,7 +297,15 @@ describe('ReactAsyncActions', () => {
// This will schedule an update on C, and also the async action scope
// will end. This will allow React to attempt to render the updates.
await act(() => resolveText('Wait before updating C'));
assertLog(['Async action ended', 'Pending: false', 'Suspend! [A1]']);
assertLog([
'Async action ended',
'Pending: false',
'Suspend! [A1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'Suspend! [A1]', 'Suspend! [B1]', 'Suspend! [C1]']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>
@@ -309,7 +317,15 @@ describe('ReactAsyncActions', () => {
// together, only when the all of A, B, and C updates are unblocked is the
// render allowed to proceed.
await act(() => resolveText('A1'));
assertLog(['Pending: false', 'A1', 'Suspend! [B1]']);
assertLog([
'Pending: false',
'A1',
'Suspend! [B1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'A1', 'Suspend! [B1]', 'Suspend! [C1]']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>
@@ -317,7 +333,16 @@ describe('ReactAsyncActions', () => {
</>,
);
await act(() => resolveText('B1'));
assertLog(['Pending: false', 'A1', 'B1', 'Suspend! [C1]']);
assertLog([
'Pending: false',
'A1',
'B1',
'Suspend! [C1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'A1', 'B1', 'Suspend! [C1]']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>
@@ -690,6 +715,10 @@ describe('ReactAsyncActions', () => {
// automatically reverted.
'Pending: false',
'Suspend! [B]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'Suspend! [B]']
: []),
]);
// Resolve the transition
@@ -231,7 +231,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
// Inner contents suspended, so we continue showing a fallback.
assertLog(['Suspend! [Inner]']);
assertLog([
'Suspend! [Inner]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Inner]'] : []),
]);
expect(root).toMatchRenderedOutput(
<>
Outer
@@ -209,7 +209,16 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<App step={2} />);
});
});
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
assertLog([
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
: []),
]);
// Because this is a refresh, we don't switch to a fallback
expect(root).toMatchRenderedOutput('A1B1');
@@ -220,7 +229,16 @@ describe('ReactConcurrentErrorRecovery', () => {
// Because we're still suspended on A, we can't show an error boundary. We
// should wait for A to resolve.
assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
assertLog([
'Suspend! [A2]',
'Loading...',
'Error! [B2]',
'Oops!',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']
: []),
]);
// Remain on previous screen.
expect(root).toMatchRenderedOutput('A1B1');
@@ -281,7 +299,16 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<App step={2} />);
});
});
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
assertLog([
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
: []),
]);
// Because this is a refresh, we don't switch to a fallback
expect(root).toMatchRenderedOutput('A1B1');
@@ -292,7 +319,16 @@ describe('ReactConcurrentErrorRecovery', () => {
// Because we're still suspended on B, we can't show an error boundary. We
// should wait for B to resolve.
assertLog(['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']);
assertLog([
'Error! [A2]',
'Oops!',
'Suspend! [B2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']
: []),
]);
// Remain on previous screen.
expect(root).toMatchRenderedOutput('A1B1');
@@ -328,7 +364,11 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<AsyncText text="Async" />);
});
});
assertLog(['Suspend! [Async]']);
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
// This also works if the suspended component is wrapped with an error
@@ -344,7 +384,11 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog(['Suspend! [Async]']);
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
// Continues rendering once data resolves
@@ -397,8 +441,14 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog(['Suspend! [Async]']);
// The render suspended without committing or surfacing the error.
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Caught an error: Oops!']
: []),
]);
// The render suspended without committing the error.
expect(root).toMatchRenderedOutput(null);
// Try the reverse order, too: throw then suspend
@@ -414,7 +464,13 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog(['Suspend! [Async]']);
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Caught an error: Oops!']
: []),
]);
expect(root).toMatchRenderedOutput(null);
await act(async () => {
@@ -420,6 +420,8 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -459,6 +461,8 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -531,6 +535,8 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -540,6 +546,8 @@ describe('ReactDeferredValue', () => {
'Loading...',
// Still waiting for the final value.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput('Loading...');
@@ -584,6 +592,8 @@ describe('ReactDeferredValue', () => {
// boundaries work, where we always prefer to show the innermost
// loading state.)
'Suspend! [Content]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Content]'] : []),
]);
// Still showing the App preview state because the inner
// content suspended.
@@ -652,7 +652,14 @@ describe('ReactExpiration', () => {
React.startTransition(() => {
root.render(<App step={1} />);
});
await waitForAll(['Suspend! [A1]', 'Loading...']);
await waitForAll([
'Suspend! [A1]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A1]', 'B', 'C', 'Loading...']
: []),
]);
// Lots of time elapses before the promise resolves
Scheduler.unstable_advanceTime(10000);
@@ -652,14 +652,22 @@ describe('ReactHooksWithNoopRenderer', () => {
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll(['Suspend!']);
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
expect(root).toMatchRenderedOutput(<span prop={0} />);
// Rendering again should suspend again.
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll(['Suspend!']);
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
});
it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
@@ -709,14 +717,22 @@ describe('ReactHooksWithNoopRenderer', () => {
setLabel('B');
});
await waitForAll(['Suspend!']);
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
// Rendering again should suspend again.
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll(['Suspend!']);
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
// Flip the signal back to "cancel" the update. However, the update to
// label should still proceed. It shouldn't have been dropped.
@@ -3495,6 +3511,13 @@ describe('ReactHooksWithNoopRenderer', () => {
'Before... Pending: true',
'Suspend! [After... Pending: false]',
'Loading... Pending: false',
...(gate('enableSiblingPrerendering')
? [
'Suspend! [After... Pending: false]',
'Loading... Pending: false',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Before... Pending: true" />,
@@ -3563,7 +3586,17 @@ describe('ReactHooksWithNoopRenderer', () => {
await act(async () => {
_setText('B');
await waitForAll(['B', 'A', 'B', 'Suspend! [B]', 'Loading']);
await waitForAll([
'B',
'A',
'B',
'Suspend! [B]',
'Loading',
...(gate('enableSiblingPrerendering')
? ['B', 'Suspend! [B]', 'Loading']
: []),
]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -4201,7 +4234,13 @@ describe('ReactHooksWithNoopRenderer', () => {
await act(async () => {
await resolveText('A');
});
assertLog(['Promise resolved [A]', 'A', 'Suspend! [B]']);
assertLog([
'Promise resolved [A]',
'A',
'Suspend! [B]',
...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
]);
await act(() => {
root.render(null);
@@ -198,7 +198,11 @@ describe('ReactLazy', () => {
await resolveFakeImport(Foo);
await waitForAll(['Foo']);
await waitForAll([
'Foo',
...(gate('enableSiblingPrerendering') ? ['Foo'] : []),
]);
expect(root).not.toMatchRenderedOutput('FooBar');
await act(() => resolveFakeImport(Bar));
@@ -235,6 +239,13 @@ describe('ReactLazy', () => {
assertConsoleErrorDev([
'Expected the result of a dynamic import() call',
'Expected the result of a dynamic import() call',
...(gate('enableSiblingPrerendering')
? [
'Expected the result of a dynamic import() call',
'Expected the result of a dynamic import() call',
]
: []),
]);
expect(root).not.toMatchRenderedOutput('Hi');
});
@@ -1072,7 +1083,7 @@ describe('ReactLazy', () => {
expect(ref.current).toBe(null);
await act(() => resolveFakeImport(Foo));
assertLog(['Foo']);
assertLog(['Foo', ...(gate('enableSiblingPrerendering') ? ['Foo'] : [])]);
await act(() => resolveFakeImport(ForwardRefBar));
assertLog(['Foo', 'forwardRef', 'Bar']);
@@ -1388,7 +1399,12 @@ describe('ReactLazy', () => {
expect(root).not.toMatchRenderedOutput('AB');
await act(() => resolveFakeImport(ChildA));
assertLog(['A', 'Init B']);
assertLog([
'A',
'Init B',
...(gate('enableSiblingPrerendering') ? ['A'] : []),
]);
await act(() => resolveFakeImport(ChildB));
assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
@@ -1472,10 +1488,20 @@ describe('ReactLazy', () => {
React.startTransition(() => {
root.update(<Parent swap={true} />);
});
await waitForAll(['Init B2', 'Loading...']);
await waitForAll([
'Init B2',
'Loading...',
...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
]);
await act(() => resolveFakeImport(ChildB2));
// We need to flush to trigger the second one to load.
assertLog(['Init A2', 'Loading...']);
assertLog([
'Init A2',
'Loading...',
...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
]);
await act(() => resolveFakeImport(ChildA2));
assertLog(['b', 'a', 'Did update: b', 'Did update: a']);
expect(root).toMatchRenderedOutput('ba');
@@ -136,6 +136,10 @@ describe('ReactSuspense', () => {
// A suspends
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Foo', 'Bar', 'Suspend! [A]', 'B', 'Loading...']
: []),
]);
expect(container.textContent).toEqual('');
@@ -275,7 +279,15 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Loading...');
await resolveText('A');
await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
await waitForAll([
'A',
'Suspend! [B]',
'Loading more...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading more...']
: []),
]);
// By this point, we have enough info to show "A" and "Loading more..."
// However, we've just shown the outer fallback. So we'll delay
@@ -327,7 +339,14 @@ describe('ReactSuspense', () => {
// B starts loading. Parent boundary is in throttle.
// Still shows parent loading under throttle
jest.advanceTimersByTime(10);
await waitForAll(['Suspend! [B]', 'Loading more...']);
await waitForAll([
'Suspend! [B]',
'Loading more...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading more...']
: []),
]);
expect(container.textContent).toEqual('Loading...');
// !! B could have finished before the throttle, but we show a fallback.
@@ -361,7 +380,15 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Loading...');
await resolveText('A');
await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
await waitForAll([
'A',
'Suspend! [B]',
'Loading more...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading more...']
: []),
]);
// By this point, we have enough info to show "A" and "Loading more..."
// However, we've just shown the outer fallback. So we'll delay
@@ -655,7 +682,14 @@ describe('ReactSuspense', () => {
assertLog(['Suspend! [Child 1]', 'Loading...']);
await resolveText('Child 1');
await waitForAll(['Child 1', 'Suspend! [Child 2]']);
await waitForAll([
'Child 1',
'Suspend! [Child 2]',
...(gate('enableSiblingPrerendering')
? ['Child 1', 'Suspend! [Child 2]']
: []),
]);
jest.advanceTimersByTime(6000);
@@ -1150,7 +1150,19 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('InnerAsync_1');
});
assertLog(['Text:Outer render', 'Suspend:OuterAsync_1']);
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
...(gate('enableSiblingPrerendering')
? [
'Text:Outer render',
'Suspend:OuterAsync_1',
'Text:Inner render',
'AsyncText:InnerAsync_1 render',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@@ -1194,6 +1206,17 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inner render',
'Suspend:InnerAsync_2',
'Text:InnerFallback render',
...(gate('enableSiblingPrerendering')
? [
'Text:Outer render',
'AsyncText:OuterAsync_1 render',
'Text:Inner render',
'Suspend:InnerAsync_2',
'Text:InnerFallback render',
]
: []),
'Text:OuterFallback destroy layout',
'Text:Outer create layout',
'AsyncText:OuterAsync_1 create layout',
@@ -2305,6 +2328,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
...(gate('enableSiblingPrerendering')
? [
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
'ClassText:Class render',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2443,7 +2475,20 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('A');
});
assertLog(['Text:Function render', 'Suspender "B" render', 'Suspend:B']);
assertLog([
'Text:Function render',
'Suspender "B" render',
'Suspend:B',
...(gate('enableSiblingPrerendering')
? [
'Text:Function render',
'Suspender "B" render',
'Suspend:B',
'ClassText:Class render',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
@@ -383,7 +383,13 @@ describe('ReactSuspenseList', () => {
);
await act(() => B.resolve());
assertLog(['A', 'B', 'Suspend! [C]']);
assertLog([
'A',
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -459,7 +465,13 @@ describe('ReactSuspenseList', () => {
);
await act(() => B.resolve());
assertLog(['A', 'B', 'Suspend! [C]']);
assertLog([
'A',
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -767,7 +779,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => B.resolve());
assertLog(['B', 'Suspend! [C]']);
assertLog([
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
]);
// Even though we could now show B, we're still waiting on C.
expect(ReactNoop).toMatchRenderedOutput(
@@ -854,7 +871,12 @@ describe('ReactSuspenseList', () => {
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
await act(() => B.resolve());
assertLog(['B', 'Suspend! [C]']);
assertLog([
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
]);
// Even though we could now show B, we're still waiting on C.
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
@@ -908,7 +930,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => A.resolve());
assertLog(['A', 'Suspend! [B]']);
assertLog([
'A',
'Suspend! [B]',
...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -967,7 +994,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => C.resolve());
assertLog(['C', 'Suspend! [B]']);
assertLog([
'C',
'Suspend! [B]',
...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [B]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1072,7 +1104,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => A.resolve());
assertLog(['A', 'Suspend! [C]']);
assertLog([
'A',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [C]'] : []),
]);
// Even though we could show A, it is still in a fallback state because
// C is not yet resolved. We need to resolve everything in the head first.
@@ -1088,7 +1125,13 @@ describe('ReactSuspenseList', () => {
);
await act(() => C.resolve());
assertLog(['A', 'C', 'Suspend! [E]']);
assertLog([
'A',
'C',
'Suspend! [E]',
...(gate('enableSiblingPrerendering') ? ['A', 'C', 'Suspend! [E]'] : []),
]);
// We can now resolve the full head.
expect(ReactNoop).toMatchRenderedOutput(
@@ -1103,7 +1146,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => E.resolve());
assertLog(['E', 'Suspend! [F]']);
assertLog([
'E',
'Suspend! [F]',
...(gate('enableSiblingPrerendering') ? ['E', 'Suspend! [F]'] : []),
]);
// In the tail we can resolve one-by-one.
expect(ReactNoop).toMatchRenderedOutput(
@@ -1267,7 +1315,12 @@ describe('ReactSuspenseList', () => {
await F.resolve();
await waitForAll(['Suspend! [D]', 'F']);
await waitForAll([
'Suspend! [D]',
'F',
...(gate('enableSiblingPrerendering') ? ['Suspend! [D]', 'F'] : []),
]);
// Even though we could show F, it is still in a fallback state because
// E is not yet resolved. We need to resolve everything in the head first.
@@ -1287,7 +1340,13 @@ describe('ReactSuspenseList', () => {
);
await act(() => D.resolve());
assertLog(['D', 'F', 'Suspend! [B]']);
assertLog([
'D',
'F',
'Suspend! [B]',
...(gate('enableSiblingPrerendering') ? ['D', 'F', 'Suspend! [B]'] : []),
]);
// We can now resolve the full head.
expect(ReactNoop).toMatchRenderedOutput(
@@ -1304,7 +1363,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => B.resolve());
assertLog(['B', 'Suspend! [A]']);
assertLog([
'B',
'Suspend! [A]',
...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [A]'] : []),
]);
// In the tail we can resolve one-by-one.
expect(ReactNoop).toMatchRenderedOutput(
@@ -1429,7 +1493,15 @@ describe('ReactSuspenseList', () => {
await A.resolve();
await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
await waitForAll([
'A',
'Suspend! [B]',
'Loading B',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading B']
: []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -1443,7 +1515,15 @@ describe('ReactSuspenseList', () => {
await B.resolve();
await waitForAll(['B', 'Suspend! [C]', 'Loading C']);
await waitForAll([
'B',
'Suspend! [C]',
'Loading C',
...(gate('enableSiblingPrerendering')
? ['B', 'Suspend! [C]', 'Loading C']
: []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -1667,7 +1747,12 @@ describe('ReactSuspenseList', () => {
await B.resolve();
await waitForAll(['B', 'Suspend! [C]']);
await waitForAll([
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -1687,7 +1772,17 @@ describe('ReactSuspenseList', () => {
await C.resolve();
await E.resolve();
await waitForAll(['B', 'C', 'E', 'Suspend! [F]', 'Loading F']);
await waitForAll([
'B',
'C',
'E',
'Suspend! [F]',
'Loading F',
...(gate('enableSiblingPrerendering')
? ['B', 'C', 'E', 'Suspend! [F]', 'Loading F']
: []),
]);
jest.advanceTimersByTime(500);
@@ -1804,7 +1899,12 @@ describe('ReactSuspenseList', () => {
await D.resolve();
await waitForAll(['D', 'Suspend! [E]']);
await waitForAll([
'D',
'Suspend! [E]',
...(gate('enableSiblingPrerendering') ? ['D', 'Suspend! [E]'] : []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -1829,7 +1929,17 @@ describe('ReactSuspenseList', () => {
await D.resolve();
await E.resolve();
await waitForAll(['D', 'E', 'B', 'Suspend! [A]', 'Loading A']);
await waitForAll([
'D',
'E',
'B',
'Suspend! [A]',
'Loading A',
...(gate('enableSiblingPrerendering')
? ['D', 'E', 'B', 'Suspend! [A]', 'Loading A']
: []),
]);
jest.advanceTimersByTime(500);
@@ -1958,7 +2068,12 @@ describe('ReactSuspenseList', () => {
await B.resolve();
await waitForAll(['B', 'Suspend! [C]']);
await waitForAll([
'B',
'Suspend! [C]',
...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -1981,7 +2096,17 @@ describe('ReactSuspenseList', () => {
await D.resolve();
await E.resolve();
await waitForAll(['C', 'D', 'E', 'Suspend! [F]', 'Loading F']);
await waitForAll([
'C',
'D',
'E',
'Suspend! [F]',
'Loading F',
...(gate('enableSiblingPrerendering')
? ['C', 'D', 'E', 'Suspend! [F]', 'Loading F']
: []),
]);
jest.advanceTimersByTime(500);
@@ -2044,7 +2169,15 @@ describe('ReactSuspenseList', () => {
await A.resolve();
await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
await waitForAll([
'A',
'Suspend! [B]',
'Loading B',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading B']
: []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -2052,7 +2185,15 @@ describe('ReactSuspenseList', () => {
expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
await act(() => B.resolve());
assertLog(['B', 'Suspend! [C]', 'Loading C']);
assertLog([
'B',
'Suspend! [C]',
'Loading C',
...(gate('enableSiblingPrerendering')
? ['B', 'Suspend! [C]', 'Loading C']
: []),
]);
// Incremental loading is suspended.
jest.advanceTimersByTime(500);
@@ -2787,7 +2928,12 @@ describe('ReactSuspenseList', () => {
expect(onRender.mock.calls[2][3]).toBe(1 + 4 + 3 + 3);
await act(() => C.resolve());
assertLog(['C', 'Suspend! [D]']);
assertLog([
'C',
'Suspend! [D]',
...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [D]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
@@ -2873,7 +3019,12 @@ describe('ReactSuspenseList', () => {
);
await act(() => A.resolve());
assertLog(['A', 'Suspend! [B]']);
assertLog([
'A',
'Suspend! [B]',
...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
@@ -298,6 +298,19 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
...(gate('enableSiblingPrerendering')
? [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
'B',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -379,7 +392,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// B suspends. Render a fallback
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
await waitForAll([
'A',
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'C', 'D', 'Loading...']
: []),
]);
// Did not commit yet.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -436,7 +457,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<App renderContent={true} />);
});
await waitForAll(['Suspend! [Result]', 'Loading...']);
await waitForAll([
'Suspend! [Result]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Result]', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
await rejectText('Result', new Error('Failed to load: Result'));
@@ -496,6 +524,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// React retries one more time
'Error! [Result]',
...(gate('enableSiblingPrerendering')
? ['Error! [Result]', 'Error! [Result]']
: []),
// Errored again on retry. Now handle it.
'Caught error: Failed to load: Result',
]);
@@ -547,7 +579,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Update the low-pri text
await act(() => startTransition(() => setLowPri('2')));
// Suspends
assertLog(['Suspend! [2]', 'Loading...']);
assertLog([
'Suspend! [2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [2]', 'Loading...']
: []),
]);
// While we're still waiting for the low-pri update to complete, update the
// high-pri text at high priority.
@@ -592,13 +631,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<App showA={true} showB={false} />);
});
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
React.startTransition(() => {
ReactNoop.render(<App showA={true} showB={true} />);
});
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'B', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
await resolveText('A');
@@ -743,6 +796,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Outer content',
'Suspend! [Inner content]',
'Loading inner...',
...(gate('enableSiblingPrerendering')
? ['Outer content', 'Suspend! [Inner content]', 'Loading inner...']
: []),
]);
// Don't commit the inner placeholder yet.
expect(ReactNoop).toMatchRenderedOutput(
@@ -932,7 +989,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Suspense>,
);
});
await waitForAll(['Suspend! [Async]', 'Loading...']);
await waitForAll([
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
// Resolve the promise
@@ -964,7 +1028,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update, and suspend for up to 5 seconds.
React.startTransition(() => ReactNoop.render(<App text="A" />));
// The update should suspend.
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
// Advance time until right before it expires.
@@ -976,7 +1047,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule another low priority update.
React.startTransition(() => ReactNoop.render(<App text="B" />));
// This update should also suspend.
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
// Schedule a regular update. Its expiration time will fall between
@@ -1747,6 +1825,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// B suspends
'Suspend! [B]',
'Loading more...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'Loading more...']
: []),
]);
// Because we've already been waiting for so long we can
// wait a bit longer. Still nothing...
@@ -2238,7 +2320,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo showB={true} />);
});
await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
await waitForAll([
'Foo',
'A',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Foo', 'A', 'Suspend! [B]', 'Loading B...']
: []),
]);
// Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
@@ -2314,7 +2405,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2365,7 +2463,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2420,7 +2525,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2462,7 +2574,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
@@ -2476,7 +2595,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start a long (infinite) transition.
React.startTransition(() => ReactNoop.render(<App page="C" />));
await waitForAll(['Suspend! [C]', 'Loading...']);
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2524,7 +2650,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
@@ -2542,7 +2675,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('C'));
await waitForAll(['Suspend! [C]', 'Loading...']);
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2594,7 +2734,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll(['Suspend! [B]', 'Loading...']);
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
@@ -2611,7 +2758,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('C'));
await waitForAll(['Suspend! [C]', 'Loading...']);
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2652,7 +2806,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
await waitForAll([
'Hi!',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Hi!', 'Suspend! [B]', 'Loading B...']
: []),
]);
// Suspended
expect(ReactNoop).toMatchRenderedOutput(
@@ -2713,7 +2875,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
await waitForAll([
'Hi!',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Hi!', 'Suspend! [B]', 'Loading B...']
: []),
]);
// Suspended
expect(ReactNoop).toMatchRenderedOutput(
@@ -2827,7 +2997,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<App showContent={true} />);
});
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await resolveText('A');
await waitFor(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -2879,7 +3056,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<App showContent={true} />);
});
await waitForAll(['Suspend! [A]', 'Loading...']);
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await resolveText('A');
await waitFor(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -3492,6 +3676,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
// This regression test relies on subtle implementation details that happen to
// rely on sibling prerendering being disabled. Not going to bother to rewrite
// it for now; maybe once we land the experiment.
// @gate !enableSiblingPrerendering
// @gate enableLegacyCache
it('regression: ping at high priority causes update to be dropped', async () => {
const {useState, useTransition} = React;
@@ -3654,7 +3842,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Update to "a". That will suspend.
await act(async () => {
setTextWithShortTransition('a');
await waitForAll(['Pending...', '', 'Suspend! [a]', 'Loading...']);
await waitForAll([
'Pending...',
'',
'Suspend! [a]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [a]', 'Loading...']
: []),
]);
});
assertLog([]);
expect(root).toMatchRenderedOutput(
@@ -3673,6 +3870,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'',
'Suspend! [b]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [b]', 'Loading...']
: []),
]);
});
assertLog([]);
@@ -3687,7 +3888,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
await resolveText('a');
await waitForAll(['Suspend! [b]', 'Loading...']);
await waitForAll([
'Suspend! [b]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [b]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
<span prop="Pending..." />
@@ -199,6 +199,10 @@ describe('ReactTransition', () => {
'(empty)',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput('Pending...(empty)');
@@ -269,6 +273,10 @@ describe('ReactTransition', () => {
'B label',
'Suspend! [B content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['B label', 'Suspend! [B content]', 'Loading...']
: []),
]);
// This is a refresh transition so it shouldn't show a fallback
expect(root).toMatchRenderedOutput(
@@ -290,6 +298,10 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -307,6 +319,10 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -394,6 +410,10 @@ describe('ReactTransition', () => {
'B label',
'Suspend! [B content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['B label', 'Suspend! [B content]', 'Loading...']
: []),
]);
// This is a refresh transition so it shouldn't show a fallback
expect(root).toMatchRenderedOutput(
@@ -415,6 +435,10 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -432,6 +456,10 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -500,7 +528,14 @@ describe('ReactTransition', () => {
setShowA(true);
});
});
assertLog(['Suspend! [A]', 'Loading...']);
assertLog([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(null);
// Before A loads, switch to B. This should entangle A with B.
@@ -510,7 +545,14 @@ describe('ReactTransition', () => {
setShowB(true);
});
});
assertLog(['Suspend! [B]', 'Loading...']);
assertLog([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(null);
// Before A or B loads, switch to C. This should entangle C with B, and
@@ -521,7 +563,14 @@ describe('ReactTransition', () => {
setShowC(true);
});
});
assertLog(['Suspend! [C]', 'Loading...']);
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(null);
// Now the data starts resolving out of order.
@@ -533,7 +582,14 @@ describe('ReactTransition', () => {
resolveText('B');
});
});
assertLog(['Suspend! [C]', 'Loading...']);
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(null);
// Now resolve A. Again, this will attempt to render C, since everything
@@ -543,7 +599,14 @@ describe('ReactTransition', () => {
resolveText('A');
});
});
assertLog(['Suspend! [C]', 'Loading...']);
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(null);
// Finally, resolve C. This time we can finish.
@@ -861,6 +924,10 @@ describe('ReactTransition', () => {
// Suspend.
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Normal pri: 0', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
+52 -8
View File
@@ -558,6 +558,7 @@ describe('ReactUse', () => {
}
});
// @gate enableSuspendingDuringWorkLoop
it('during a transition, can unwrap async operations even if nothing is cached', async () => {
function App() {
return <Text text={use(getAsyncText('Async'))} />;
@@ -593,6 +594,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('Async');
});
// @gate enableSuspendingDuringWorkLoop
it("does not prevent a Suspense fallback from showing if it's a new boundary, even during a transition", async () => {
function App() {
return <Text text={use(getAsyncText('Async'))} />;
@@ -635,6 +637,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('Async');
});
// @gate enableSuspendingDuringWorkLoop
it('when waiting for data to resolve, a fresh update will trigger a restart', async () => {
function App() {
return <Text text={use(getAsyncText('Will never resolve'))} />;
@@ -666,6 +669,7 @@ describe('ReactUse', () => {
assertLog(['Something different']);
});
// @gate enableSuspendingDuringWorkLoop
it('when waiting for data to resolve, an update on a different root does not cause work to be dropped', async () => {
const promise = getAsyncText('Hi');
@@ -708,6 +712,7 @@ describe('ReactUse', () => {
expect(root1).toMatchRenderedOutput('Hi');
});
// @gate enableSuspendingDuringWorkLoop
it('while suspended, hooks cannot be called (i.e. current dispatcher is unset correctly)', async () => {
function App() {
return <Text text={use(getAsyncText('Will never resolve'))} />;
@@ -845,6 +850,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('(empty)');
});
// @gate enableSuspendingDuringWorkLoop
it('when replaying a suspended component, reuses the hooks computed during the previous attempt (Memo)', async () => {
function ExcitingText({text}) {
// This computes the uppercased version of some text. Pretend it's an
@@ -894,6 +900,7 @@ describe('ReactUse', () => {
]);
});
// @gate enableSuspendingDuringWorkLoop
it('when replaying a suspended component, reuses the hooks computed during the previous attempt (State)', async () => {
let _setFruit;
let _setVegetable;
@@ -950,6 +957,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('banana dill');
});
// @gate enableSuspendingDuringWorkLoop
it('when replaying a suspended component, reuses the hooks computed during the previous attempt (DebugValue+State)', async () => {
// Make sure we don't get a Hook mismatch warning on updates if there were non-stateful Hooks before the use().
let _setLawyer;
@@ -991,6 +999,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('aguacate avocat');
});
// @gate enableSuspendingDuringWorkLoop
it(
'wrap an async function with useMemo to skip running the function ' +
'twice when loading new data',
@@ -1057,13 +1066,25 @@ describe('ReactUse', () => {
await act(() => {
resolveTextRequests('A');
});
assertLog(['A', '(Loading B...)']);
assertLog([
'A',
'(Loading B...)',
...(gate('enableSiblingPrerendering')
? ['A', '(Loading C...)', '(Loading B...)']
: []),
]);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {
resolveTextRequests('B');
});
assertLog(['B', '(Loading C...)']);
assertLog([
'B',
'(Loading C...)',
...(gate('enableSiblingPrerendering') ? ['B', '(Loading C...)'] : []),
]);
expect(root).toMatchRenderedOutput('AB(Loading C...)');
await act(() => {
@@ -1073,6 +1094,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('ABC');
});
// @gate enableSuspendingDuringWorkLoop
it('load multiple nested Suspense boundaries (uncached requests)', async () => {
// This the same as the previous test, except the requests are not cached.
// The tree should still eventually resolve, despite the
@@ -1196,6 +1218,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('Hi');
});
// @gate enableSuspendingDuringWorkLoop
it('basic async component', async () => {
async function App() {
await getAsyncText('Hi');
@@ -1220,6 +1243,7 @@ describe('ReactUse', () => {
expect(root).toMatchRenderedOutput('Hi');
});
// @gate enableSuspendingDuringWorkLoop
it('async child of a non-function component (e.g. a class)', async () => {
class App extends React.Component {
async render() {
@@ -1856,16 +1880,26 @@ describe('ReactUse', () => {
await expect(async () => {
await act(() => resolveTextRequests('Hi'));
}).toErrorDev(
}).toErrorDev([
// We get this warning because the generator's promise themselves are not cached.
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
);
...(gate('enableSiblingPrerendering')
? ['A component was suspended by an uncached promise.']
: []),
]);
assertLog(['Async text requested [World]']);
await act(() => resolveTextRequests('World'));
if (gate('enableSiblingPrerendering')) {
await expect(async () => {
await act(() => resolveTextRequests('World'));
}).toErrorDev(['A component was suspended by an uncached promise.']);
} else {
await act(() => resolveTextRequests('World'));
}
assertLog(['Hi', 'World']);
expect(root).toMatchRenderedOutput('Hi World');
@@ -1901,16 +1935,26 @@ describe('ReactUse', () => {
await expect(async () => {
await act(() => resolveTextRequests('Hi'));
}).toErrorDev(
}).toErrorDev([
// We get this warning because the generator's promise themselves are not cached.
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
);
...(gate('enableSiblingPrerendering')
? ['A component was suspended by an uncached promise.']
: []),
]);
assertLog(['Async text requested [World]']);
await act(() => resolveTextRequests('World'));
if (gate('enableSiblingPrerendering')) {
await expect(async () => {
await act(() => resolveTextRequests('World'));
}).toErrorDev(['A component was suspended by an uncached promise.']);
} else {
await act(() => resolveTextRequests('World'));
}
assertLog(['Hi', 'World']);
expect(root).toMatchRenderedOutput(<div>Hi World</div>);
@@ -559,13 +559,26 @@ describe('useMemoCache()', () => {
root.render(<App chunkA={updatedChunkA} chunkB={updatedChunkB} />);
});
});
assertLog(['Suspend! [chunkA]']);
assertLog([
'Suspend! [chunkA]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkA]'] : []),
]);
// The data starts to stream in. Loading the data in the first chunk
// triggers an expensive computation in the UI. Later, we'll test whether
// this computation is reused.
await act(() => updatedChunkA.resolve('A2'));
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
assertLog([
'Some expensive processing... [A2]',
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering')
? gate('enableNoCloningMemoCache')
? ['Suspend! [chunkB]']
: ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
: []),
]);
// The second chunk hasn't loaded yet, so we're still showing the
// initial UI.
@@ -586,11 +599,22 @@ describe('useMemoCache()', () => {
if (gate(flags => flags.enableNoCloningMemoCache)) {
// We did not have process the first chunk again. We reused the
// computation from the earlier attempt.
assertLog(['Suspend! [chunkB]']);
assertLog([
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkB]'] : []),
]);
} else {
// Because we clone/reset the memo cache after every aborted attempt, we
// must process the first chunk again.
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
assertLog([
'Some expensive processing... [A2]',
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering')
? ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
: []),
]);
}
expect(root).toMatchRenderedOutput(
@@ -274,6 +274,10 @@ describe('useSyncExternalStore', () => {
// This should a synchronous re-render of A using the updated value. In
// this test, this causes A to suspend.
'Suspend A',
...(gate('enableSiblingPrerendering')
? ['Suspend A', 'B: Updated']
: []),
]);
// Nothing has committed, because A suspended and no fallback
// was provided.
+4
View File
@@ -83,6 +83,10 @@ function getTestFlags() {
enableActivity: releaseChannel === 'experimental' || www || xplat,
enableSuspenseList: releaseChannel === 'experimental' || www || xplat,
enableLegacyHidden: www,
// TODO: Suspending the work loop during the render phase is currently
// not compatible with sibling prerendering. We will add this optimization
// back in a later step.
enableSuspendingDuringWorkLoop: !featureFlags.enableSiblingPrerendering,
// This flag is used to determine whether we should run Fizz tests using
// the external runtime or the inline script runtime.