experimental_use(promise) (#25084)

* Internal `act`: Unwrapping resolved promises

This update our internal implementation of `act` to support React's new
behavior for unwrapping promises. Like we did with Scheduler, when 
something suspends, it will yield to the main thread so the microtasks
can run, then continue in a new task.

I need to implement the same behavior in the public version of `act`,
but there are some additional considerations so I'll do that in a
separate commit.

* Move throwException to after work loop resumes

throwException is the function that finds the nearest boundary and
schedules it for a second render pass. We should only call it right 
before we unwind the stack — not if we receive an immediate ping and
render the fiber again.

This was an oversight in 6eaadc1 that I didn't notice because it happens
to mostly work, anyway. What made me notice the mistake is that
throwException also marks the entire render phase as suspended
(RootDidSuspend or RootDidSuspendWithDelay), which is only supposed to
be happen if we show a fallback. One consequence was that, in the 
RootDidSuspendWithDelay case, the entire commit phase was blocked,
because that's the exit status we use to block a bad fallback
from appearing.

* Use expando to check whether promise has resolved

Add a `status` expando to a thrown thenable to track when its value has
resolved.

In a later step, we'll also use `value` and `reason` expandos to track
the resolved value.

This is not part of the official JavaScript spec — think of
it as an extension of the Promise API, or a custom interface that is a
superset of Thenable. However, it's inspired by the terminology used
by `Promise.allSettled`.

The intent is that this will be a public API — Suspense implementations
can set these expandos to allow React to unwrap the value synchronously
without waiting a microtask.

* Scaffolding for `experimental_use` hook

Sets up a new experimental hook behind a feature flag, but does not
implement it yet.

* use(promise)

Adds experimental support to Fiber for unwrapping the value of a promise
inside a component. It is not yet implemented for Server Components, 
but that is planned.

If promise has already resolved, the value can be unwrapped
"immediately" without showing a fallback. The trick we use to implement
this is to yield to the main thread (literally suspending the work
loop), wait for the microtask queue to drain, then check if the promise
resolved in the meantime. If so, we can resume the last attempted fiber
without unwinding the stack. This functionality was implemented in 
previous commits.

Another feature is that the promises do not need to be cached between
attempts. Because we assume idempotent execution of components, React
will track the promises that were used during the previous attempt and
reuse the result. You shouldn't rely on this property, but during
initial render it mostly just works. Updates are trickier, though,
because if you used an uncached promise, we have no way of knowing 
whether the underlying data has changed, so we have to unwrap the
promise every time. It will still work, but it's inefficient and can
lead to unnecessary fallbacks if it happens during a discrete update.

When we implement this for Server Components, this will be less of an
issue because there are no updates in that environment. However, it's
still better for performance to cache data requests, so the same
principles largely apply.

The intention is that this will eventually be the only supported way to
suspend on arbitrary promises. Throwing a promise directly will
be deprecated.
This commit is contained in:
Andrew Clark
2022-08-25 14:12:07 -04:00
committed by GitHub
parent 6c1db1a3a0
commit f478ccd179
33 changed files with 1402 additions and 443 deletions
+24 -12
View File
@@ -23,7 +23,7 @@ import enqueueTask from 'shared/enqueueTask';
let actingUpdatesScopeDepth = 0;
export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
if (Scheduler.unstable_flushUntilNextPaint === undefined) {
throw Error(
'This version of `act` requires a special mock build of Scheduler.',
);
@@ -120,19 +120,31 @@ export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
}
function flushActWork(resolve, reject) {
// Flush suspended fallbacks
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
enqueueTask(() => {
if (Scheduler.unstable_hasPendingWork()) {
try {
const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
if (didFlushWork) {
flushActWork(resolve, reject);
} else {
resolve();
}
Scheduler.unstable_flushUntilNextPaint();
} catch (error) {
reject(error);
}
});
// If Scheduler yields while there's still work, it's so that we can
// unblock the main thread (e.g. for paint or for microtasks). Yield to
// the main thread and continue in a new task.
enqueueTask(() => flushActWork(resolve, reject));
return;
}
// Once the scheduler queue is empty, run all the timers. The purpose of this
// is to force any pending fallbacks to commit. The public version of act does
// this with dev-only React runtime logic, but since our internal act needs to
// work work production builds of React, we have to cheat.
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
if (Scheduler.unstable_hasPendingWork()) {
// Committing a fallback scheduled additional work. Continue flushing.
flushActWork(resolve, reject);
return;
}
resolve();
}
@@ -13,6 +13,8 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
Usable,
Thenable,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';
@@ -32,6 +34,7 @@ import {
enableLazyContextPropagation,
enableUseMutableSource,
enableTransitionTracing,
enableUseHook,
enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
@@ -120,6 +123,10 @@ import {
} from './ReactFiberConcurrentUpdates.new';
import {getTreeId} from './ReactFiberTreeContext.new';
import {now} from './Scheduler';
import {
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberWakeable.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -205,6 +212,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
// Counts number of `use`-d thenables
let thenableIndexCounter: number = 0;
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
@@ -403,6 +413,7 @@ export function renderWithHooks<Props, SecondArg>(
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// thenableIndexCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -441,6 +452,7 @@ export function renderWithHooks<Props, SecondArg>(
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -524,6 +536,7 @@ export function renderWithHooks<Props, SecondArg>(
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
thenableIndexCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -631,6 +644,7 @@ export function resetHooksAfterThrow(): void {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -722,6 +736,73 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
};
}
function use<T>(usable: Usable<T>): T {
if (
usable !== null &&
typeof usable === 'object' &&
typeof usable.then === 'function'
) {
// This is a thenable.
const thenable: Thenable<T> = (usable: any);
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
throw rejectedError;
}
default: {
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
index,
);
if (prevThenableAtIndex !== null) {
switch (prevThenableAtIndex.status) {
case 'fulfilled': {
const fulfilledValue: T = prevThenableAtIndex.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError: mixed = prevThenableAtIndex.reason;
throw rejectedError;
}
default: {
// The thenable still hasn't resolved. Suspend with the same
// thenable as last time to avoid redundant listeners.
throw prevThenableAtIndex;
}
}
} else {
// This is the first time something has been used at this index.
// Stash the thenable at the current index so we can reuse it during
// the next attempt.
trackUsedThenable(thenable, index);
// Suspend.
// TODO: Throwing here is an implementation detail that allows us to
// unwind the call stack. But we shouldn't allow it to leak into
// userspace. Throw an opaque placeholder value instead of the
// actual thenable. If it doesn't get captured by the work loop, log
// a warning, because that means something in userspace must have
// caught it.
throw thenable;
}
}
}
}
// TODO: Add support for Context
// eslint-disable-next-line react-internal/safe-string-coercion
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
function useMemoCache(size: number): Array<any> {
throw new Error('Not implemented.');
}
@@ -2421,6 +2502,9 @@ if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
if (enableUseHook) {
(ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
}
if (enableUseMemoCacheHook) {
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
}
@@ -2452,6 +2536,9 @@ if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnMount: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2485,6 +2572,9 @@ if (enableCache) {
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
}
if (enableUseHook) {
(HooksDispatcherOnUpdate: Dispatcher).use = use;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -2513,6 +2603,9 @@ if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnRerender: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2691,6 +2784,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2836,6 +2932,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2981,6 +3080,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -3127,6 +3229,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -3289,6 +3394,14 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -3456,6 +3569,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -3624,6 +3745,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -13,6 +13,8 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
Usable,
Thenable,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.old';
@@ -32,6 +34,7 @@ import {
enableLazyContextPropagation,
enableUseMutableSource,
enableTransitionTracing,
enableUseHook,
enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
@@ -120,6 +123,10 @@ import {
} from './ReactFiberConcurrentUpdates.old';
import {getTreeId} from './ReactFiberTreeContext.old';
import {now} from './Scheduler';
import {
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberWakeable.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -205,6 +212,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
// Counts number of `use`-d thenables
let thenableIndexCounter: number = 0;
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
@@ -403,6 +413,7 @@ export function renderWithHooks<Props, SecondArg>(
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// thenableIndexCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -441,6 +452,7 @@ export function renderWithHooks<Props, SecondArg>(
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -524,6 +536,7 @@ export function renderWithHooks<Props, SecondArg>(
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
thenableIndexCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -631,6 +644,7 @@ export function resetHooksAfterThrow(): void {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -722,6 +736,73 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
};
}
function use<T>(usable: Usable<T>): T {
if (
usable !== null &&
typeof usable === 'object' &&
typeof usable.then === 'function'
) {
// This is a thenable.
const thenable: Thenable<T> = (usable: any);
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
throw rejectedError;
}
default: {
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
index,
);
if (prevThenableAtIndex !== null) {
switch (prevThenableAtIndex.status) {
case 'fulfilled': {
const fulfilledValue: T = prevThenableAtIndex.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError: mixed = prevThenableAtIndex.reason;
throw rejectedError;
}
default: {
// The thenable still hasn't resolved. Suspend with the same
// thenable as last time to avoid redundant listeners.
throw prevThenableAtIndex;
}
}
} else {
// This is the first time something has been used at this index.
// Stash the thenable at the current index so we can reuse it during
// the next attempt.
trackUsedThenable(thenable, index);
// Suspend.
// TODO: Throwing here is an implementation detail that allows us to
// unwind the call stack. But we shouldn't allow it to leak into
// userspace. Throw an opaque placeholder value instead of the
// actual thenable. If it doesn't get captured by the work loop, log
// a warning, because that means something in userspace must have
// caught it.
throw thenable;
}
}
}
}
// TODO: Add support for Context
// eslint-disable-next-line react-internal/safe-string-coercion
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
function useMemoCache(size: number): Array<any> {
throw new Error('Not implemented.');
}
@@ -2421,6 +2502,9 @@ if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
if (enableUseHook) {
(ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
}
if (enableUseMemoCacheHook) {
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
}
@@ -2452,6 +2536,9 @@ if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnMount: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2485,6 +2572,9 @@ if (enableCache) {
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
}
if (enableUseHook) {
(HooksDispatcherOnUpdate: Dispatcher).use = use;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -2513,6 +2603,9 @@ if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnRerender: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2691,6 +2784,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2836,6 +2932,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -2981,6 +3080,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -3127,6 +3229,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
}
@@ -3289,6 +3394,14 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -3456,6 +3569,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -3624,6 +3745,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
size: number,
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
let lanes = pendingLanes;
//
// We exclude retry lanes because those must always be time sliced, in order
// to unwrap uncached promises.
// TODO: Write a test for this
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
return getHighestPriorityLanes(root.pendingLanes);
}
export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
export function getLanesToRetrySynchronouslyOnError(
root: FiberRoot,
originallyAttemptedLanes: Lanes,
): Lanes {
if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
// The error recovery mechanism is disabled until these lanes are cleared.
return NoLanes;
}
const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
if (everythingButOffscreen !== NoLanes) {
return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.entangledLanes &= remainingLanes;
root.errorRecoveryDisabledLanes &= remainingLanes;
const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
let lanes = pendingLanes;
//
// We exclude retry lanes because those must always be time sliced, in order
// to unwrap uncached promises.
// TODO: Write a test for this
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
return getHighestPriorityLanes(root.pendingLanes);
}
export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
export function getLanesToRetrySynchronouslyOnError(
root: FiberRoot,
originallyAttemptedLanes: Lanes,
): Lanes {
if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
// The error recovery mechanism is disabled until these lanes are cleared.
return NoLanes;
}
const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
if (everythingButOffscreen !== NoLanes) {
return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.entangledLanes &= remainingLanes;
root.errorRecoveryDisabledLanes &= remainingLanes;
const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;
@@ -70,6 +70,7 @@ function FiberRootNode(
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.errorRecoveryDisabledLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
@@ -70,6 +70,7 @@ function FiberRootNode(
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.errorRecoveryDisabledLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
@@ -57,7 +57,7 @@ import {
onUncaughtError,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
attachPingListener,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.new';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
@@ -78,8 +78,6 @@ import {
queueHydrationError,
} from './ReactFiberHydrationContext.new';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
return update;
}
function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
// Attach a ping listener
//
// The data might resolve before we have a chance to commit the fallback. Or,
// in the case of a refresh, we'll never commit a fallback. So we need to
// attach a listener now. When it resolves ("pings"), we can decide whether to
// try rendering the tree again.
//
// Only attach a listener if one does not already exist for the lanes
// we're currently rendering (which acts like a "thread ID" here).
//
// We only need to do this in concurrent mode. Legacy Suspense always
// commits fallbacks synchronously, so there are no pings.
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
} else {
threadIDs = pingCache.get(wakeable);
if (threadIDs === undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}
if (!threadIDs.has(lanes)) {
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
if (enableLazyContextPropagation) {
const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
): Wakeable | null {
): void {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
@@ -459,7 +417,7 @@ function throwException(
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
return wakeable;
return;
} else {
// No boundary was found. Unless this is a sync update, this is OK.
// We can suspend and wait for more data to arrive.
@@ -474,7 +432,7 @@ function throwException(
// This case also applies to initial hydration.
attachPingListener(root, wakeable, rootRenderLanes);
renderDidSuspendDelayIfPossible();
return wakeable;
return;
}
// This is a sync/discrete update. We treat this case like an error
@@ -517,7 +475,7 @@ function throwException(
// Even though the user may not be affected by this error, we should
// still log it so it can be fixed.
queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
return null;
return;
}
} else {
// Otherwise, fall through to the error path.
@@ -540,7 +498,7 @@ function throwException(
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return null;
return;
}
case ClassComponent:
// Capture and retry
@@ -564,7 +522,7 @@ function throwException(
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return null;
return;
}
break;
default:
@@ -572,7 +530,6 @@ function throwException(
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
return null;
}
export {throwException, createRootErrorUpdate, createClassErrorUpdate};
@@ -57,7 +57,7 @@ import {
onUncaughtError,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
attachPingListener,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.old';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
@@ -78,8 +78,6 @@ import {
queueHydrationError,
} from './ReactFiberHydrationContext.old';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
return update;
}
function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
// Attach a ping listener
//
// The data might resolve before we have a chance to commit the fallback. Or,
// in the case of a refresh, we'll never commit a fallback. So we need to
// attach a listener now. When it resolves ("pings"), we can decide whether to
// try rendering the tree again.
//
// Only attach a listener if one does not already exist for the lanes
// we're currently rendering (which acts like a "thread ID" here).
//
// We only need to do this in concurrent mode. Legacy Suspense always
// commits fallbacks synchronously, so there are no pings.
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
} else {
threadIDs = pingCache.get(wakeable);
if (threadIDs === undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}
if (!threadIDs.has(lanes)) {
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
if (enableLazyContextPropagation) {
const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
): Wakeable | null {
): void {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
@@ -459,7 +417,7 @@ function throwException(
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
return wakeable;
return;
} else {
// No boundary was found. Unless this is a sync update, this is OK.
// We can suspend and wait for more data to arrive.
@@ -474,7 +432,7 @@ function throwException(
// This case also applies to initial hydration.
attachPingListener(root, wakeable, rootRenderLanes);
renderDidSuspendDelayIfPossible();
return wakeable;
return;
}
// This is a sync/discrete update. We treat this case like an error
@@ -517,7 +475,7 @@ function throwException(
// Even though the user may not be affected by this error, we should
// still log it so it can be fixed.
queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
return null;
return;
}
} else {
// Otherwise, fall through to the error path.
@@ -540,7 +498,7 @@ function throwException(
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return null;
return;
}
case ClassComponent:
// Capture and retry
@@ -564,7 +522,7 @@ function throwException(
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return null;
return;
}
break;
default:
@@ -572,7 +530,6 @@ function throwException(
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
return null;
}
export {throwException, createRootErrorUpdate, createClassErrorUpdate};
@@ -7,38 +7,104 @@
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {
Wakeable,
Thenable,
PendingThenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
let suspendedWakeable: Wakeable | null = null;
let wasPinged = false;
let suspendedThenable: Thenable<mixed> | null = null;
let adHocSuspendCount: number = 0;
let usedThenables: Array<Thenable<any> | void> | null = null;
let lastUsedThenable: Thenable<any> | null = null;
const MAX_AD_HOC_SUSPEND_COUNT = 50;
export function suspendedWakeableWasPinged() {
return wasPinged;
export function isTrackingSuspendedThenable() {
return suspendedThenable !== null;
}
export function trackSuspendedWakeable(wakeable: Wakeable) {
adHocSuspendCount++;
suspendedWakeable = wakeable;
}
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
if (wakeable === suspendedWakeable) {
// This ping is from the wakeable that just suspended. Mark it as pinged.
// When the work loop resumes, we'll immediately try rendering the fiber
// again instead of unwinding the stack.
wasPinged = true;
return true;
export function suspendedThenableDidResolve() {
if (suspendedThenable !== null) {
const status = suspendedThenable.status;
return status === 'fulfilled' || status === 'rejected';
}
return false;
}
export function resetWakeableState() {
suspendedWakeable = null;
wasPinged = false;
export function trackSuspendedWakeable(wakeable: Wakeable) {
// If this wakeable isn't already a thenable, turn it into one now. Then,
// when we resume the work loop, we can check if its status is
// still pending.
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
const thenable: Thenable<mixed> = (wakeable: any);
if (thenable !== lastUsedThenable) {
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
// that was thrown by an older Suspense implementation. Keep a count of
// these so that we can detect an infinite ping loop.
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
// a better way to count ad hoc suspends is whether an actual thenable
// is caught by the work loop.
adHocSuspendCount++;
}
suspendedThenable = thenable;
// We use an expando to track the status and result of a thenable so that we
// can synchronously unwrap the value. Think of this as an extension of the
// Promise API, or a custom interface that is a superset of Thenable.
//
// If the thenable doesn't have a status, set it to "pending" and attach
// a listener that will update its status and result when it resolves.
switch (thenable.status) {
case 'pending':
// Since the status is already "pending", we can assume it will be updated
// when it resolves, either by React or something in userspace.
break;
case 'fulfilled':
case 'rejected':
// A thenable that already resolved shouldn't have been thrown, so this is
// unexpected. Suggests a mistake in a userspace data library. Don't track
// this thenable, because if we keep trying it will likely infinite loop
// without ever resolving.
// TODO: Log a warning?
suspendedThenable = null;
break;
default: {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function resetWakeableStateAfterEachAttempt() {
suspendedThenable = null;
adHocSuspendCount = 0;
lastUsedThenable = null;
}
export function resetThenableStateOnCompletion() {
usedThenables = null;
}
export function throwIfInfinitePingLoopDetected() {
@@ -48,3 +114,23 @@ export function throwIfInfinitePingLoopDetected() {
// the render phase so that it gets the component stack.
}
}
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (usedThenables === null) {
usedThenables = [];
}
usedThenables[index] = thenable;
lastUsedThenable = thenable;
}
export function getPreviouslyUsedThenableAtIndex<T>(
index: number,
): Thenable<T> | null {
if (usedThenables !== null) {
const thenable = usedThenables[index];
if (thenable !== undefined) {
return thenable;
}
}
return null;
}
@@ -7,38 +7,104 @@
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {
Wakeable,
Thenable,
PendingThenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
let suspendedWakeable: Wakeable | null = null;
let wasPinged = false;
let suspendedThenable: Thenable<mixed> | null = null;
let adHocSuspendCount: number = 0;
let usedThenables: Array<Thenable<any> | void> | null = null;
let lastUsedThenable: Thenable<any> | null = null;
const MAX_AD_HOC_SUSPEND_COUNT = 50;
export function suspendedWakeableWasPinged() {
return wasPinged;
export function isTrackingSuspendedThenable() {
return suspendedThenable !== null;
}
export function trackSuspendedWakeable(wakeable: Wakeable) {
adHocSuspendCount++;
suspendedWakeable = wakeable;
}
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
if (wakeable === suspendedWakeable) {
// This ping is from the wakeable that just suspended. Mark it as pinged.
// When the work loop resumes, we'll immediately try rendering the fiber
// again instead of unwinding the stack.
wasPinged = true;
return true;
export function suspendedThenableDidResolve() {
if (suspendedThenable !== null) {
const status = suspendedThenable.status;
return status === 'fulfilled' || status === 'rejected';
}
return false;
}
export function resetWakeableState() {
suspendedWakeable = null;
wasPinged = false;
export function trackSuspendedWakeable(wakeable: Wakeable) {
// If this wakeable isn't already a thenable, turn it into one now. Then,
// when we resume the work loop, we can check if its status is
// still pending.
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
const thenable: Thenable<mixed> = (wakeable: any);
if (thenable !== lastUsedThenable) {
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
// that was thrown by an older Suspense implementation. Keep a count of
// these so that we can detect an infinite ping loop.
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
// a better way to count ad hoc suspends is whether an actual thenable
// is caught by the work loop.
adHocSuspendCount++;
}
suspendedThenable = thenable;
// We use an expando to track the status and result of a thenable so that we
// can synchronously unwrap the value. Think of this as an extension of the
// Promise API, or a custom interface that is a superset of Thenable.
//
// If the thenable doesn't have a status, set it to "pending" and attach
// a listener that will update its status and result when it resolves.
switch (thenable.status) {
case 'pending':
// Since the status is already "pending", we can assume it will be updated
// when it resolves, either by React or something in userspace.
break;
case 'fulfilled':
case 'rejected':
// A thenable that already resolved shouldn't have been thrown, so this is
// unexpected. Suggests a mistake in a userspace data library. Don't track
// this thenable, because if we keep trying it will likely infinite loop
// without ever resolving.
// TODO: Log a warning?
suspendedThenable = null;
break;
default: {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function resetWakeableStateAfterEachAttempt() {
suspendedThenable = null;
adHocSuspendCount = 0;
lastUsedThenable = null;
}
export function resetThenableStateOnCompletion() {
usedThenables = null;
}
export function throwIfInfinitePingLoopDetected() {
@@ -48,3 +114,23 @@ export function throwIfInfinitePingLoopDetected() {
// the render phase so that it gets the component stack.
}
}
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (usedThenables === null) {
usedThenables = [];
}
usedThenables[index] = thenable;
lastUsedThenable = thenable;
}
export function getPreviouslyUsedThenableAtIndex<T>(
index: number,
): Thenable<T> | null {
if (usedThenables !== null) {
const thenable = usedThenables[index];
if (thenable !== undefined) {
return thenable;
}
}
return null;
}
@@ -254,14 +254,17 @@ import {
} from './ReactFiberAct.new';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
import {
resetWakeableState,
resetWakeableStateAfterEachAttempt,
resetThenableStateOnCompletion,
trackSuspendedWakeable,
suspendedWakeableWasPinged,
attemptToPingSuspendedWakeable,
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.new';
const ceil = Math.ceil;
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
@@ -299,6 +302,12 @@ let workInProgressRootRenderLanes: Lanes = NoLanes;
// after this happens. If the fiber is pinged before we resume, we can retry
// immediately instead of unwinding the stack.
let workInProgressIsSuspended: boolean = false;
let workInProgressThrownValue: mixed = null;
// 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).
let workInProgressRootDidAttachPingListener: boolean = false;
// A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree
@@ -1013,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
if (exitStatus === RootFatalErrored) {
@@ -1056,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// We need to check again if something threw
if (exitStatus === RootErrored) {
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
@@ -1090,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
return null;
}
function recoverFromConcurrentError(root, errorRetryLanes) {
function recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
// Before rendering again, save the errors from the previous attempt.
const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
if (isRootDehydrated(root)) {
const wasRootDehydrated = isRootDehydrated(root);
if (wasRootDehydrated) {
// The shell failed to hydrate. Set a flag to force a client rendering
// during the next attempt. To do this, we call prepareFreshStack now
// to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1120,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
// During the synchronous render, we attached additional ping listeners.
// This is highly suggestive of an uncached promise (though it's not the
// only reason this would happen). If it was an uncached promise, then
// it may have masked a downstream error from ocurring without actually
// fixing it. Example:
//
// use(Promise.resolve('uncached'))
// throw new Error('Oops!')
//
// When this happens, there's a conflict between blocking potential
// concurrent data races and unwrapping uncached promise values. We
// have to choose one or the other. Because the data race recovery is
// a last ditch effort, we'll disable it.
root.errorRecoveryDisabledLanes = mergeLanes(
root.errorRecoveryDisabledLanes,
originallyAttemptedLanes,
);
// Mark the current render as suspended and force it to restart. Once
// these lanes finish successfully, we'll re-enable the error recovery
// mechanism for subsequent updates.
workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
return RootSuspendedWithDelay;
}
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
@@ -1376,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
@@ -1596,13 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
interruptedWork = interruptedWork.return;
}
resetWakeableState();
resetWakeableStateAfterEachAttempt();
resetThenableStateOnCompletion();
}
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
@@ -1621,94 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress;
}
function handleError(root, thrownValue): Wakeable | null {
do {
let erroredWork = workInProgress;
try {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
function handleThrow(root, thrownValue): void {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return null;
}
// Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
workInProgressThrownValue = thrownValue;
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
// suspended render.
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
const erroredWork = workInProgress;
if (erroredWork === null) {
// This is a fatal error
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
return;
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
const isWakeable =
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function';
if (
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function'
) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
wakeable,
workInProgressRootRenderLanes,
);
} else {
markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
}
}
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
// suspended render.
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
const maybeWakeable = throwException(
root,
erroredWork.return,
if (enableSchedulingProfiler) {
markComponentRenderStopped();
if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
wakeable,
workInProgressRootRenderLanes,
);
} else {
markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
// Return to the normal work loop.
return maybeWakeable;
} catch (yetAnotherThrownValue) {
// Something in the return path also threw.
thrownValue = yetAnotherThrownValue;
if (workInProgress === erroredWork && erroredWork !== null) {
// If this boundary has already errored, then we had trouble processing
// the error. Bubble it to the next boundary.
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
} while (true);
}
if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);
trackSuspendedWakeable(wakeable);
}
}
function pushDispatcher() {
@@ -1834,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
handleThrow(root, thrownValue);
}
} while (true);
resetContextDependencies();
@@ -1872,10 +1910,15 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
if (workInProgressIsSuspended && workInProgress !== null) {
if (workInProgressIsSuspended) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
const thrownValue = workInProgressThrownValue;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
if (workInProgress !== null) {
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
}
}
while (workInProgress !== null) {
@@ -1927,12 +1970,11 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent();
break;
} catch (thrownValue) {
const maybeWakeable = handleError(root, thrownValue);
if (maybeWakeable !== null) {
handleThrow(root, thrownValue);
if (isTrackingSuspendedThenable()) {
// If this fiber just suspended, it's possible the data is already
// cached. Yield to the the main thread to give it a chance to ping. If
// it does, we can retry immediately without unwinding the stack.
trackSuspendedWakeable(maybeWakeable);
break;
}
}
@@ -1974,10 +2016,15 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
if (workInProgressIsSuspended && workInProgress !== null) {
if (workInProgressIsSuspended) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
const thrownValue = workInProgressThrownValue;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
if (workInProgress !== null) {
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
}
}
while (workInProgress !== null && !shouldYield()) {
@@ -2013,27 +2060,72 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null;
}
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
function resumeSuspendedUnitOfWork(
unitOfWork: Fiber,
thrownValue: mixed,
): void {
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
// just suspended. In some cases, we may choose to retry the fiber immediately
// instead of unwinding the stack. It's a separate function to keep the
// additional logic out of the work loop's hot path.
if (!suspendedWakeableWasPinged()) {
// The wakeable wasn't pinged. Return to the normal work loop. This will
const wasPinged = suspendedThenableDidResolve();
resetWakeableStateAfterEachAttempt();
if (!wasPinged) {
// The thenable wasn't pinged. Return to the normal work loop. This will
// unwind the stack, and potentially result in showing a fallback.
workInProgressIsSuspended = false;
resetWakeableState();
resetThenableStateOnCompletion();
const returnFiber = unitOfWork.return;
if (returnFiber === null || workInProgressRoot === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return;
}
try {
// Find and mark the nearest Suspense or error boundary that can handle
// this "exception".
throwException(
workInProgressRoot,
returnFiber,
unitOfWork,
thrownValue,
workInProgressRootRenderLanes,
);
} catch (error) {
// We had trouble processing the error. An example of this happening is
// when accessing the `componentDidCatch` property of an error boundary
// throws an error. A weird edge case. There's a regression test for this.
// To prevent an infinite loop, bubble the error up to the next parent.
workInProgress = returnFiber;
throw error;
}
// Return to the normal work loop.
completeUnitOfWork(unitOfWork);
return;
}
// The work-in-progress was immediately pinged. Instead of unwinding the
// stack and potentially showing a fallback, reset the fiber and try rendering
// it again.
// stack and potentially showing a fallback, unwind only the last stack frame,
// reset the fiber, and try rendering it again.
const current = unitOfWork.alternate;
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
@@ -2048,8 +2140,7 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
// The begin phase finished successfully without suspending. Reset the state
// used to track the fiber while it was suspended. Then return to the normal
// work loop.
workInProgressIsSuspended = false;
resetWakeableState();
resetThenableStateOnCompletion();
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2850,7 +2941,53 @@ export function captureCommitPhaseError(
}
}
export function pingSuspendedRoot(
export function attachPingListener(
root: FiberRoot,
wakeable: Wakeable,
lanes: Lanes,
) {
// Attach a ping listener
//
// The data might resolve before we have a chance to commit the fallback. Or,
// in the case of a refresh, we'll never commit a fallback. So we need to
// attach a listener now. When it resolves ("pings"), we can decide whether to
// try rendering the tree again.
//
// Only attach a listener if one does not already exist for the lanes
// we're currently rendering (which acts like a "thread ID" here).
//
// We only need to do this in concurrent mode. Legacy Suspense always
// commits fallbacks synchronously, so there are no pings.
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
} else {
threadIDs = pingCache.get(wakeable);
if (threadIDs === undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}
if (!threadIDs.has(lanes)) {
workInProgressRootDidAttachPingListener = true;
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
function pingSuspendedRoot(
root: FiberRoot,
wakeable: Wakeable,
pingedLanes: Lanes,
@@ -2874,31 +3011,26 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
if (didPingSuspendedWakeable) {
// Successfully pinged the in-progress fiber. Don't unwind the stack.
} else {
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
}
@@ -3172,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
throw originalError;
}
// Keep this code in sync with handleError; any changes here must have
// Keep this code in sync with handleThrow; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooksAfterThrow();
@@ -254,14 +254,17 @@ import {
} from './ReactFiberAct.old';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
import {
resetWakeableState,
resetWakeableStateAfterEachAttempt,
resetThenableStateOnCompletion,
trackSuspendedWakeable,
suspendedWakeableWasPinged,
attemptToPingSuspendedWakeable,
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.old';
const ceil = Math.ceil;
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
@@ -299,6 +302,12 @@ let workInProgressRootRenderLanes: Lanes = NoLanes;
// after this happens. If the fiber is pinged before we resume, we can retry
// immediately instead of unwinding the stack.
let workInProgressIsSuspended: boolean = false;
let workInProgressThrownValue: mixed = null;
// 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).
let workInProgressRootDidAttachPingListener: boolean = false;
// A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree
@@ -1013,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
if (exitStatus === RootFatalErrored) {
@@ -1056,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// We need to check again if something threw
if (exitStatus === RootErrored) {
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
@@ -1090,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
return null;
}
function recoverFromConcurrentError(root, errorRetryLanes) {
function recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
// Before rendering again, save the errors from the previous attempt.
const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
if (isRootDehydrated(root)) {
const wasRootDehydrated = isRootDehydrated(root);
if (wasRootDehydrated) {
// The shell failed to hydrate. Set a flag to force a client rendering
// during the next attempt. To do this, we call prepareFreshStack now
// to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1120,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
// During the synchronous render, we attached additional ping listeners.
// This is highly suggestive of an uncached promise (though it's not the
// only reason this would happen). If it was an uncached promise, then
// it may have masked a downstream error from ocurring without actually
// fixing it. Example:
//
// use(Promise.resolve('uncached'))
// throw new Error('Oops!')
//
// When this happens, there's a conflict between blocking potential
// concurrent data races and unwrapping uncached promise values. We
// have to choose one or the other. Because the data race recovery is
// a last ditch effort, we'll disable it.
root.errorRecoveryDisabledLanes = mergeLanes(
root.errorRecoveryDisabledLanes,
originallyAttemptedLanes,
);
// Mark the current render as suspended and force it to restart. Once
// these lanes finish successfully, we'll re-enable the error recovery
// mechanism for subsequent updates.
workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
return RootSuspendedWithDelay;
}
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
@@ -1376,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
@@ -1596,13 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
interruptedWork = interruptedWork.return;
}
resetWakeableState();
resetWakeableStateAfterEachAttempt();
resetThenableStateOnCompletion();
}
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
@@ -1621,94 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress;
}
function handleError(root, thrownValue): Wakeable | null {
do {
let erroredWork = workInProgress;
try {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
function handleThrow(root, thrownValue): void {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return null;
}
// Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
workInProgressThrownValue = thrownValue;
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
// suspended render.
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
const erroredWork = workInProgress;
if (erroredWork === null) {
// This is a fatal error
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
return;
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
const isWakeable =
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function';
if (
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function'
) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
wakeable,
workInProgressRootRenderLanes,
);
} else {
markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
}
}
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
// suspended render.
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
const maybeWakeable = throwException(
root,
erroredWork.return,
if (enableSchedulingProfiler) {
markComponentRenderStopped();
if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
wakeable,
workInProgressRootRenderLanes,
);
} else {
markComponentErrored(
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
// Return to the normal work loop.
return maybeWakeable;
} catch (yetAnotherThrownValue) {
// Something in the return path also threw.
thrownValue = yetAnotherThrownValue;
if (workInProgress === erroredWork && erroredWork !== null) {
// If this boundary has already errored, then we had trouble processing
// the error. Bubble it to the next boundary.
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
} while (true);
}
if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);
trackSuspendedWakeable(wakeable);
}
}
function pushDispatcher() {
@@ -1834,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
handleThrow(root, thrownValue);
}
} while (true);
resetContextDependencies();
@@ -1872,10 +1910,15 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
if (workInProgressIsSuspended && workInProgress !== null) {
if (workInProgressIsSuspended) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
const thrownValue = workInProgressThrownValue;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
if (workInProgress !== null) {
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
}
}
while (workInProgress !== null) {
@@ -1927,12 +1970,11 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent();
break;
} catch (thrownValue) {
const maybeWakeable = handleError(root, thrownValue);
if (maybeWakeable !== null) {
handleThrow(root, thrownValue);
if (isTrackingSuspendedThenable()) {
// If this fiber just suspended, it's possible the data is already
// cached. Yield to the the main thread to give it a chance to ping. If
// it does, we can retry immediately without unwinding the stack.
trackSuspendedWakeable(maybeWakeable);
break;
}
}
@@ -1974,10 +2016,15 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
if (workInProgressIsSuspended && workInProgress !== null) {
if (workInProgressIsSuspended) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
const thrownValue = workInProgressThrownValue;
workInProgressIsSuspended = false;
workInProgressThrownValue = null;
if (workInProgress !== null) {
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
}
}
while (workInProgress !== null && !shouldYield()) {
@@ -2013,27 +2060,72 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null;
}
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
function resumeSuspendedUnitOfWork(
unitOfWork: Fiber,
thrownValue: mixed,
): void {
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
// just suspended. In some cases, we may choose to retry the fiber immediately
// instead of unwinding the stack. It's a separate function to keep the
// additional logic out of the work loop's hot path.
if (!suspendedWakeableWasPinged()) {
// The wakeable wasn't pinged. Return to the normal work loop. This will
const wasPinged = suspendedThenableDidResolve();
resetWakeableStateAfterEachAttempt();
if (!wasPinged) {
// The thenable wasn't pinged. Return to the normal work loop. This will
// unwind the stack, and potentially result in showing a fallback.
workInProgressIsSuspended = false;
resetWakeableState();
resetThenableStateOnCompletion();
const returnFiber = unitOfWork.return;
if (returnFiber === null || workInProgressRoot === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return;
}
try {
// Find and mark the nearest Suspense or error boundary that can handle
// this "exception".
throwException(
workInProgressRoot,
returnFiber,
unitOfWork,
thrownValue,
workInProgressRootRenderLanes,
);
} catch (error) {
// We had trouble processing the error. An example of this happening is
// when accessing the `componentDidCatch` property of an error boundary
// throws an error. A weird edge case. There's a regression test for this.
// To prevent an infinite loop, bubble the error up to the next parent.
workInProgress = returnFiber;
throw error;
}
// Return to the normal work loop.
completeUnitOfWork(unitOfWork);
return;
}
// The work-in-progress was immediately pinged. Instead of unwinding the
// stack and potentially showing a fallback, reset the fiber and try rendering
// it again.
// stack and potentially showing a fallback, unwind only the last stack frame,
// reset the fiber, and try rendering it again.
const current = unitOfWork.alternate;
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
@@ -2048,8 +2140,7 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
// The begin phase finished successfully without suspending. Reset the state
// used to track the fiber while it was suspended. Then return to the normal
// work loop.
workInProgressIsSuspended = false;
resetWakeableState();
resetThenableStateOnCompletion();
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2850,7 +2941,53 @@ export function captureCommitPhaseError(
}
}
export function pingSuspendedRoot(
export function attachPingListener(
root: FiberRoot,
wakeable: Wakeable,
lanes: Lanes,
) {
// Attach a ping listener
//
// The data might resolve before we have a chance to commit the fallback. Or,
// in the case of a refresh, we'll never commit a fallback. So we need to
// attach a listener now. When it resolves ("pings"), we can decide whether to
// try rendering the tree again.
//
// Only attach a listener if one does not already exist for the lanes
// we're currently rendering (which acts like a "thread ID" here).
//
// We only need to do this in concurrent mode. Legacy Suspense always
// commits fallbacks synchronously, so there are no pings.
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
} else {
threadIDs = pingCache.get(wakeable);
if (threadIDs === undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}
if (!threadIDs.has(lanes)) {
workInProgressRootDidAttachPingListener = true;
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
function pingSuspendedRoot(
root: FiberRoot,
wakeable: Wakeable,
pingedLanes: Lanes,
@@ -2874,31 +3011,26 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
if (didPingSuspendedWakeable) {
// Successfully pinged the in-progress fiber. Don't unwind the stack.
} else {
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
}
@@ -3172,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
throw originalError;
}
// Keep this code in sync with handleError; any changes here must have
// Keep this code in sync with handleThrow; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooksAfterThrow();
+3
View File
@@ -17,6 +17,7 @@ import type {
MutableSource,
StartTransitionOptions,
Wakeable,
Usable,
} from 'shared/ReactTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {WorkTag} from './ReactWorkTags';
@@ -238,6 +239,7 @@ type BaseFiberRootProperties = {|
pingedLanes: Lanes,
expiredLanes: Lanes,
mutableReadLanes: Lanes,
errorRecoveryDisabledLanes: Lanes,
finishedLanes: Lanes,
@@ -353,6 +355,7 @@ type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
export type Dispatcher = {|
use?: <T>(Usable<T>) => T,
getCacheSignal?: () => AbortSignal,
getCacheForType?: <T>(resourceType: () => T) => T,
readContext<T>(context: ReactContext<T>): T,
+264 -13
View File
@@ -4,6 +4,7 @@ let React;
let ReactNoop;
let Scheduler;
let act;
let use;
let Suspense;
let startTransition;
@@ -15,6 +16,7 @@ describe('ReactWakeable', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('jest-react').act;
use = React.experimental_use;
Suspense = React.Suspense;
startTransition = React.startTransition;
});
@@ -45,23 +47,272 @@ describe('ReactWakeable', () => {
);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
ReactNoop.render(<App />);
root.render(<App />);
});
// React will yield when the async component suspends.
expect(Scheduler).toFlushUntilNextPaint(['Suspend!']);
// Wait for microtasks to resolve
// TODO: The async form of `act` should automatically yield to microtasks
// when a continuation is returned, the way Scheduler does.
await null;
expect(Scheduler).toHaveYielded(['Resolve in microtask']);
});
// Finished rendering without unwinding the stack.
expect(Scheduler).toHaveYielded(['Async']);
expect(Scheduler).toHaveYielded([
// React will yield when the async component suspends.
'Suspend!',
'Resolve in microtask',
// Finished rendering without unwinding the stack or preparing a fallback.
'Async',
]);
expect(root).toMatchRenderedOutput('Async');
});
test('if suspended fiber is pinged in a microtask, it does not block a transition from completing', async () => {
let resolved = false;
function Async() {
if (resolved) {
return <Text text="Async" />;
}
Scheduler.unstable_yieldValue('Suspend!');
throw Promise.resolve().then(() => {
Scheduler.unstable_yieldValue('Resolve in microtask');
resolved = true;
});
}
function App() {
return <Async />;
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
expect(Scheduler).toHaveYielded([
'Suspend!',
'Resolve in microtask',
'Async',
]);
expect(root).toMatchRenderedOutput('Async');
});
test('does not infinite loop if already resolved thenable is thrown', async () => {
// An already resolved promise should never be thrown. Since it already
// resolved, we shouldn't bother trying to render again — doing so would
// likely lead to an infinite loop. This scenario should only happen if a
// userspace Suspense library makes an implementation mistake.
// Create an already resolved thenable
const thenable = {
then(ping) {},
status: 'fulfilled',
value: null,
};
let i = 0;
function Async() {
if (i++ > 50) {
throw new Error('Infinite loop detected');
}
Scheduler.unstable_yieldValue('Suspend!');
// This thenable should never be thrown because it already resolved.
// But if it is thrown, React should handle it gracefully.
throw thenable;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Suspend!', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
});
// @gate enableUseHook
test('basic use(promise)', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.resolve('B');
const promiseC = Promise.resolve('C');
function Async() {
const text = use(promiseA) + use(promiseB) + use(promiseC);
return <Text text={text} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
expect(Scheduler).toHaveYielded(['ABC']);
expect(root).toMatchRenderedOutput('ABC');
});
// @gate enableUseHook
test("using a promise that's not cached between attempts", async () => {
function Async() {
const text =
use(Promise.resolve('A')) +
use(Promise.resolve('B')) +
use(Promise.resolve('C'));
return <Text text={text} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
expect(Scheduler).toHaveYielded(['ABC']);
expect(root).toMatchRenderedOutput('ABC');
});
// @gate enableUseHook
test('using a rejected promise will throw', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <Text text={this.state.error.message} />;
}
return this.props.children;
}
}
const promiseA = Promise.resolve('A');
const promiseB = Promise.reject(new Error('Oops!'));
const promiseC = Promise.resolve('C');
// Jest/Node will raise an unhandled rejected error unless we await this. It
// works fine in the browser, though.
await expect(promiseB).rejects.toThrow('Oops!');
function Async() {
const text = use(promiseA) + use(promiseB) + use(promiseC);
return <Text text={text} />;
}
function App() {
return (
<ErrorBoundary>
<Async />
</ErrorBoundary>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
expect(Scheduler).toHaveYielded(['Oops!', 'Oops!']);
});
// @gate enableUseHook
test('erroring in the same component as an uncached promise does not result in an infinite loop', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <Text text={'Caught an error: ' + this.state.error.message} />;
}
return this.props.children;
}
}
let i = 0;
function Async({
// Intentionally destrucutring a prop here so that our production error
// stack trick is triggered at the beginning of the function
prop,
}) {
if (i++ > 50) {
throw new Error('Infinite loop detected');
}
try {
use(Promise.resolve('Async'));
} catch (e) {
Scheduler.unstable_yieldValue('Suspend! [Async]');
throw e;
}
throw new Error('Oops!');
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<ErrorBoundary>
<Async />
</ErrorBoundary>
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
expect(Scheduler).toHaveYielded([
// First attempt. The uncached promise suspends.
'Suspend! [Async]',
// Because the promise already resolved, we're able to unwrap the value
// immediately in a microtask.
//
// Then we proceed to the rest of the component, which throws an error.
'Caught an error: Oops!',
// During the sync error recovery pass, the component suspends, because
// we were unable to unwrap the value of the promise.
'Suspend! [Async]',
'Loading...',
// Because the error recovery attempt suspended, React can't tell if the
// error was actually fixed, or it was masked by the suspended data.
// In this case, it wasn't actually fixed, so if we were to commit the
// suspended fallback, it would enter an endless error recovery loop.
//
// Instead, we disable error recovery for these lanes and start
// over again.
// This time, the error is thrown and we commit the result.
'Suspend! [Async]',
'Caught an error: Oops!',
]);
expect(root).toMatchRenderedOutput('Caught an error: Oops!');
});
});
+1
View File
@@ -27,6 +27,7 @@ export {
createMutableSource as unstable_createMutableSource,
createRef,
createServerContext,
experimental_use,
forwardRef,
isValidElement,
lazy,
+1
View File
@@ -24,6 +24,7 @@ export {
createFactory,
createRef,
createServerContext,
experimental_use,
forwardRef,
isValidElement,
lazy,
+1
View File
@@ -49,6 +49,7 @@ export {
createMutableSource,
createRef,
createServerContext,
experimental_use,
forwardRef,
isValidElement,
lazy,
+1
View File
@@ -26,6 +26,7 @@ export {
createMutableSource as unstable_createMutableSource,
createRef,
createServerContext,
experimental_use,
forwardRef,
isValidElement,
lazy,
+2
View File
@@ -55,6 +55,7 @@ import {
useDeferredValue,
useId,
useCacheRefresh,
use,
useMemoCache,
} from './ReactHooks';
import {
@@ -128,6 +129,7 @@ export {
getCacheForType as unstable_getCacheForType,
useCacheRefresh as unstable_useCacheRefresh,
REACT_CACHE_TYPE as unstable_Cache,
use as experimental_use,
useMemoCache as unstable_useMemoCache,
// enableScopeAPI
REACT_SCOPE_TYPE as unstable_Scope,
+7
View File
@@ -14,6 +14,7 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
Usable,
} from 'shared/ReactTypes';
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
@@ -205,6 +206,12 @@ export function useCacheRefresh(): <T>(?() => T, ?T) => void {
return dispatcher.useCacheRefresh();
}
export function use<T>(usable: Usable<T>): T {
const dispatcher = resolveDispatcher();
// $FlowFixMe This is unstable, thus optional
return dispatcher.use(usable);
}
export function useMemoCache(size: number): Array<any> {
const dispatcher = resolveDispatcher();
// $FlowFixMe This is unstable, thus optional
@@ -517,6 +517,11 @@ function unstable_flushUntilNextPaint(): void {
isFlushing = false;
}
}
return false;
}
function unstable_hasPendingWork(): boolean {
return scheduledCallback !== null;
}
function unstable_flushExpired() {
@@ -644,6 +649,7 @@ export {
unstable_flushExpired,
unstable_clearYields,
unstable_flushUntilNextPaint,
unstable_hasPendingWork,
unstable_flushAll,
unstable_yieldValue,
unstable_advanceTime,
+1
View File
@@ -117,6 +117,7 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
export const deletedTreeCleanUpLevel = 3;
export const enableFloat = __EXPERIMENTAL__;
export const enableUseHook = __EXPERIMENTAL__;
// Enables unstable_useMemoCache hook, intended as a compilation target for
// auto-memoization.
+31 -5
View File
@@ -174,12 +174,35 @@ export interface Wakeable {
// The subset of a Promise that React APIs rely on. This resolves a value.
// This doesn't require a return value neither from the handler nor the
// then function.
export interface Thenable<+R> {
then<U>(
onFulfill: (value: R) => void | Thenable<U> | U,
onReject: (error: mixed) => void | Thenable<U> | U,
): void | Thenable<U>;
interface ThenableImpl<T> {
then(
onFulfill: (value: T) => mixed,
onReject: (error: mixed) => mixed,
): void | Wakeable;
}
interface UntrackedThenable<T> extends ThenableImpl<T> {
status?: void;
}
export interface PendingThenable<T> extends ThenableImpl<T> {
status: 'pending';
}
export interface FulfilledThenable<T> extends ThenableImpl<T> {
status: 'fulfilled';
value: T;
}
export interface RejectedThenable<T> extends ThenableImpl<T> {
status: 'rejected';
reason: mixed;
}
export type Thenable<T> =
| UntrackedThenable<T>
| PendingThenable<T>
| FulfilledThenable<T>
| RejectedThenable<T>;
export type OffscreenMode =
| 'hidden'
@@ -189,3 +212,6 @@ export type OffscreenMode =
export type StartTransitionOptions = {
name?: string,
};
// TODO: Add Context support
export type Usable<T> = Thenable<T>;
@@ -50,6 +50,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = true;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = true;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const enableSuspenseAvoidThisFallback = true;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = true;
export const enableUseHook = false;
export const enableUseMemoCacheHook = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
export const enableClientRenderFallbackOnTextMismatch = true;
@@ -53,6 +53,7 @@ export const enableSuspenseAvoidThisFallback = true;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = true;
export const enableFloat = false;
export const enableUseHook = true;
export const enableUseMemoCacheHook = true;
// Logs additional User Timing API marks for use with an experimental profiling tool.
+2 -1
View File
@@ -422,5 +422,6 @@
"434": "`dangerouslySetInnerHTML` does not make sense on <title>.",
"435": "Unexpected Suspense handler tag (%s). This is a bug in React.",
"436": "Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of \"%s\".",
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead."
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
"438": "An unsupported type was passed to use(): %s"
}