mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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+.
|
||||
|
||||
+479
-262
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
+91
-2
@@ -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
|
||||
|
||||
+3
-2
@@ -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':
|
||||
|
||||
+39
-8
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+65
-9
@@ -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);
|
||||
|
||||
+45
-6
@@ -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);
|
||||
|
||||
|
||||
+47
-2
@@ -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} />
|
||||
|
||||
+175
-24
@@ -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>
|
||||
|
||||
+232
-24
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user