@samitouri / QOS-React-2 / commits / b6978bc38f

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 8ef3a7c 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.

Andrew Clark committed Aug 25, 2022 at 14:12 UTC b6978bc38f6788c7e73982b9fd2771aabdf36f15
33 files changed +1398 -439
packages/jest-react/src/internalAct.js
+24 -12
@@ -23,7 +23,7 @@ import enqueueTask from 'shared/enqueueTask';
23 let actingUpdatesScopeDepth = 0;
24
25 export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
26 - if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
26 + if (Scheduler.unstable_flushUntilNextPaint === undefined) {
27 throw Error(
28 'This version of `act` requires a special mock build of Scheduler.',
29 );
@@ -120,19 +120,31 @@ export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
120 }
121
122 function flushActWork(resolve, reject) {
123 - // Flush suspended fallbacks
124 - // $FlowFixMe: Flow doesn't know about global Jest object
125 - jest.runOnlyPendingTimers();
126 - enqueueTask(() => {
123 + if (Scheduler.unstable_hasPendingWork()) {
124 try {
128 - const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
129 - if (didFlushWork) {
130 - flushActWork(resolve, reject);
131 - } else {
132 - resolve();
133 - }
125 + Scheduler.unstable_flushUntilNextPaint();
126 } catch (error) {
127 reject(error);
128 }
137 - });
129 +
130 + // If Scheduler yields while there's still work, it's so that we can
131 + // unblock the main thread (e.g. for paint or for microtasks). Yield to
132 + // the main thread and continue in a new task.
133 + enqueueTask(() => flushActWork(resolve, reject));
134 + return;
135 + }
136 +
137 + // Once the scheduler queue is empty, run all the timers. The purpose of this
138 + // is to force any pending fallbacks to commit. The public version of act does
139 + // this with dev-only React runtime logic, but since our internal act needs to
140 + // work work production builds of React, we have to cheat.
141 + // $FlowFixMe: Flow doesn't know about global Jest object
142 + jest.runOnlyPendingTimers();
143 + if (Scheduler.unstable_hasPendingWork()) {
144 + // Committing a fallback scheduled additional work. Continue flushing.
145 + flushActWork(resolve, reject);
146 + return;
147 + }
148 +
149 + resolve();
150 }
packages/react-reconciler/src/ReactFiberHooks.new.js
+129
@@ -13,6 +13,8 @@ import type {
13 MutableSourceSubscribeFn,
14 ReactContext,
15 StartTransitionOptions,
16 + Usable,
17 + Thenable,
18 } from 'shared/ReactTypes';
19 import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
20 import type {Lanes, Lane} from './ReactFiberLane.new';
@@ -32,6 +34,7 @@ import {
34 enableLazyContextPropagation,
35 enableUseMutableSource,
36 enableTransitionTracing,
37 + enableUseHook,
38 enableUseMemoCacheHook,
39 } from 'shared/ReactFeatureFlags';
40
@@ -120,6 +123,10 @@ import {
123 } from './ReactFiberConcurrentUpdates.new';
124 import {getTreeId} from './ReactFiberTreeContext.new';
125 import {now} from './Scheduler';
126 +import {
127 + trackUsedThenable,
128 + getPreviouslyUsedThenableAtIndex,
129 +} from './ReactFiberWakeable.new';
130
131 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
132
@@ -205,6 +212,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
212 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
213 // Counts the number of useId hooks in this component.
214 let localIdCounter: number = 0;
215 +// Counts number of `use`-d thenables
216 +let thenableIndexCounter: number = 0;
217 +
218 // Used for ids that are generated completely client-side (i.e. not during
219 // hydration). This counter is global, so client ids are not stable across
220 // render attempts.
@@ -403,6 +413,7 @@ export function renderWithHooks<Props, SecondArg>(
413
414 // didScheduleRenderPhaseUpdate = false;
415 // localIdCounter = 0;
416 + // thenableIndexCounter = 0;
417
418 // TODO Warn if no hooks are used at all during mount, then some are used during update.
419 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -441,6 +452,7 @@ export function renderWithHooks<Props, SecondArg>(
452 do {
453 didScheduleRenderPhaseUpdateDuringThisPass = false;
454 localIdCounter = 0;
455 + thenableIndexCounter = 0;
456
457 if (numberOfReRenders >= RE_RENDER_LIMIT) {
458 throw new Error(
@@ -524,6 +536,7 @@ export function renderWithHooks<Props, SecondArg>(
536 didScheduleRenderPhaseUpdate = false;
537 // This is reset by checkDidRenderIdHook
538 // localIdCounter = 0;
539 + thenableIndexCounter = 0;
540
541 if (didRenderTooFewHooks) {
542 throw new Error(
@@ -631,6 +644,7 @@ export function resetHooksAfterThrow(): void {
644
645 didScheduleRenderPhaseUpdateDuringThisPass = false;
646 localIdCounter = 0;
647 + thenableIndexCounter = 0;
648 }
649
650 function mountWorkInProgressHook(): Hook {
@@ -722,6 +736,73 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
736 };
737 }
738
739 +function use<T>(usable: Usable<T>): T {
740 + if (
741 + usable !== null &&
742 + typeof usable === 'object' &&
743 + typeof usable.then === 'function'
744 + ) {
745 + // This is a thenable.
746 + const thenable: Thenable<T> = (usable: any);
747 +
748 + // Track the position of the thenable within this fiber.
749 + const index = thenableIndexCounter;
750 + thenableIndexCounter += 1;
751 +
752 + switch (thenable.status) {
753 + case 'fulfilled': {
754 + const fulfilledValue: T = thenable.value;
755 + return fulfilledValue;
756 + }
757 + case 'rejected': {
758 + const rejectedError = thenable.reason;
759 + throw rejectedError;
760 + }
761 + default: {
762 + const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
763 + index,
764 + );
765 + if (prevThenableAtIndex !== null) {
766 + switch (prevThenableAtIndex.status) {
767 + case 'fulfilled': {
768 + const fulfilledValue: T = prevThenableAtIndex.value;
769 + return fulfilledValue;
770 + }
771 + case 'rejected': {
772 + const rejectedError: mixed = prevThenableAtIndex.reason;
773 + throw rejectedError;
774 + }
775 + default: {
776 + // The thenable still hasn't resolved. Suspend with the same
777 + // thenable as last time to avoid redundant listeners.
778 + throw prevThenableAtIndex;
779 + }
780 + }
781 + } else {
782 + // This is the first time something has been used at this index.
783 + // Stash the thenable at the current index so we can reuse it during
784 + // the next attempt.
785 + trackUsedThenable(thenable, index);
786 +
787 + // Suspend.
788 + // TODO: Throwing here is an implementation detail that allows us to
789 + // unwind the call stack. But we shouldn't allow it to leak into
790 + // userspace. Throw an opaque placeholder value instead of the
791 + // actual thenable. If it doesn't get captured by the work loop, log
792 + // a warning, because that means something in userspace must have
793 + // caught it.
794 + throw thenable;
795 + }
796 + }
797 + }
798 + }
799 +
800 + // TODO: Add support for Context
801 +
802 + // eslint-disable-next-line react-internal/safe-string-coercion
803 + throw new Error('An unsupported type was passed to use(): ' + String(usable));
804 +}
805 +
806 function useMemoCache(size: number): Array<any> {
807 throw new Error('Not implemented.');
808 }
@@ -2421,6 +2502,9 @@ if (enableCache) {
2502 (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
2503 (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
2504 }
2505 +if (enableUseHook) {
2506 + (ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
2507 +}
2508 if (enableUseMemoCacheHook) {
2509 (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
2510 }
@@ -2452,6 +2536,9 @@ if (enableCache) {
2536 (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
2537 (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
2538 }
2539 +if (enableUseHook) {
2540 + (HooksDispatcherOnMount: Dispatcher).use = use;
2541 +}
2542 if (enableUseMemoCacheHook) {
2543 (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
2544 }
@@ -2485,6 +2572,9 @@ if (enableCache) {
2572 if (enableUseMemoCacheHook) {
2573 (HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
2574 }
2575 +if (enableUseHook) {
2576 + (HooksDispatcherOnUpdate: Dispatcher).use = use;
2577 +}
2578
2579 const HooksDispatcherOnRerender: Dispatcher = {
2580 readContext,
@@ -2513,6 +2603,9 @@ if (enableCache) {
2603 (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
2604 (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
2605 }
2606 +if (enableUseHook) {
2607 + (HooksDispatcherOnRerender: Dispatcher).use = use;
2608 +}
2609 if (enableUseMemoCacheHook) {
2610 (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
2611 }
@@ -2691,6 +2784,9 @@ if (__DEV__) {
2784 return mountRefresh();
2785 };
2786 }
2787 + if (enableUseHook) {
2788 + (HooksDispatcherOnMountInDEV: Dispatcher).use = use;
2789 + }
2790 if (enableUseMemoCacheHook) {
2791 (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2792 }
@@ -2836,6 +2932,9 @@ if (__DEV__) {
2932 return mountRefresh();
2933 };
2934 }
2935 + if (enableUseHook) {
2936 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
2937 + }
2938 if (enableUseMemoCacheHook) {
2939 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
2940 }
@@ -2981,6 +3080,9 @@ if (__DEV__) {
3080 return updateRefresh();
3081 };
3082 }
3083 + if (enableUseHook) {
3084 + (HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
3085 + }
3086 if (enableUseMemoCacheHook) {
3087 (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3088 }
@@ -3127,6 +3229,9 @@ if (__DEV__) {
3229 return updateRefresh();
3230 };
3231 }
3232 + if (enableUseHook) {
3233 + (HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
3234 + }
3235 if (enableUseMemoCacheHook) {
3236 (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
3237 }
@@ -3289,6 +3394,14 @@ if (__DEV__) {
3394 return mountRefresh();
3395 };
3396 }
3397 + if (enableUseHook) {
3398 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function<T>(
3399 + usable: Usable<T>,
3400 + ): T {
3401 + warnInvalidHookAccess();
3402 + return use(usable);
3403 + };
3404 + }
3405 if (enableUseMemoCacheHook) {
3406 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
3407 size: number,
@@ -3456,6 +3569,14 @@ if (__DEV__) {
3569 return updateRefresh();
3570 };
3571 }
3572 + if (enableUseHook) {
3573 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function<T>(
3574 + usable: Usable<T>,
3575 + ): T {
3576 + warnInvalidHookAccess();
3577 + return use(usable);
3578 + };
3579 + }
3580 if (enableUseMemoCacheHook) {
3581 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
3582 size: number,
@@ -3624,6 +3745,14 @@ if (__DEV__) {
3745 return updateRefresh();
3746 };
3747 }
3748 + if (enableUseHook) {
3749 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function<T>(
3750 + usable: Usable<T>,
3751 + ): T {
3752 + warnInvalidHookAccess();
3753 + return use(usable);
3754 + };
3755 + }
3756 if (enableUseMemoCacheHook) {
3757 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
3758 size: number,
packages/react-reconciler/src/ReactFiberHooks.old.js
+129
@@ -13,6 +13,8 @@ import type {
13 MutableSourceSubscribeFn,
14 ReactContext,
15 StartTransitionOptions,
16 + Usable,
17 + Thenable,
18 } from 'shared/ReactTypes';
19 import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
20 import type {Lanes, Lane} from './ReactFiberLane.old';
@@ -32,6 +34,7 @@ import {
34 enableLazyContextPropagation,
35 enableUseMutableSource,
36 enableTransitionTracing,
37 + enableUseHook,
38 enableUseMemoCacheHook,
39 } from 'shared/ReactFeatureFlags';
40
@@ -120,6 +123,10 @@ import {
123 } from './ReactFiberConcurrentUpdates.old';
124 import {getTreeId} from './ReactFiberTreeContext.old';
125 import {now} from './Scheduler';
126 +import {
127 + trackUsedThenable,
128 + getPreviouslyUsedThenableAtIndex,
129 +} from './ReactFiberWakeable.old';
130
131 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
132
@@ -205,6 +212,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
212 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
213 // Counts the number of useId hooks in this component.
214 let localIdCounter: number = 0;
215 +// Counts number of `use`-d thenables
216 +let thenableIndexCounter: number = 0;
217 +
218 // Used for ids that are generated completely client-side (i.e. not during
219 // hydration). This counter is global, so client ids are not stable across
220 // render attempts.
@@ -403,6 +413,7 @@ export function renderWithHooks<Props, SecondArg>(
413
414 // didScheduleRenderPhaseUpdate = false;
415 // localIdCounter = 0;
416 + // thenableIndexCounter = 0;
417
418 // TODO Warn if no hooks are used at all during mount, then some are used during update.
419 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -441,6 +452,7 @@ export function renderWithHooks<Props, SecondArg>(
452 do {
453 didScheduleRenderPhaseUpdateDuringThisPass = false;
454 localIdCounter = 0;
455 + thenableIndexCounter = 0;
456
457 if (numberOfReRenders >= RE_RENDER_LIMIT) {
458 throw new Error(
@@ -524,6 +536,7 @@ export function renderWithHooks<Props, SecondArg>(
536 didScheduleRenderPhaseUpdate = false;
537 // This is reset by checkDidRenderIdHook
538 // localIdCounter = 0;
539 + thenableIndexCounter = 0;
540
541 if (didRenderTooFewHooks) {
542 throw new Error(
@@ -631,6 +644,7 @@ export function resetHooksAfterThrow(): void {
644
645 didScheduleRenderPhaseUpdateDuringThisPass = false;
646 localIdCounter = 0;
647 + thenableIndexCounter = 0;
648 }
649
650 function mountWorkInProgressHook(): Hook {
@@ -722,6 +736,73 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
736 };
737 }
738
739 +function use<T>(usable: Usable<T>): T {
740 + if (
741 + usable !== null &&
742 + typeof usable === 'object' &&
743 + typeof usable.then === 'function'
744 + ) {
745 + // This is a thenable.
746 + const thenable: Thenable<T> = (usable: any);
747 +
748 + // Track the position of the thenable within this fiber.
749 + const index = thenableIndexCounter;
750 + thenableIndexCounter += 1;
751 +
752 + switch (thenable.status) {
753 + case 'fulfilled': {
754 + const fulfilledValue: T = thenable.value;
755 + return fulfilledValue;
756 + }
757 + case 'rejected': {
758 + const rejectedError = thenable.reason;
759 + throw rejectedError;
760 + }
761 + default: {
762 + const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
763 + index,
764 + );
765 + if (prevThenableAtIndex !== null) {
766 + switch (prevThenableAtIndex.status) {
767 + case 'fulfilled': {
768 + const fulfilledValue: T = prevThenableAtIndex.value;
769 + return fulfilledValue;
770 + }
771 + case 'rejected': {
772 + const rejectedError: mixed = prevThenableAtIndex.reason;
773 + throw rejectedError;
774 + }
775 + default: {
776 + // The thenable still hasn't resolved. Suspend with the same
777 + // thenable as last time to avoid redundant listeners.
778 + throw prevThenableAtIndex;
779 + }
780 + }
781 + } else {
782 + // This is the first time something has been used at this index.
783 + // Stash the thenable at the current index so we can reuse it during
784 + // the next attempt.
785 + trackUsedThenable(thenable, index);
786 +
787 + // Suspend.
788 + // TODO: Throwing here is an implementation detail that allows us to
789 + // unwind the call stack. But we shouldn't allow it to leak into
790 + // userspace. Throw an opaque placeholder value instead of the
791 + // actual thenable. If it doesn't get captured by the work loop, log
792 + // a warning, because that means something in userspace must have
793 + // caught it.
794 + throw thenable;
795 + }
796 + }
797 + }
798 + }
799 +
800 + // TODO: Add support for Context
801 +
802 + // eslint-disable-next-line react-internal/safe-string-coercion
803 + throw new Error('An unsupported type was passed to use(): ' + String(usable));
804 +}
805 +
806 function useMemoCache(size: number): Array<any> {
807 throw new Error('Not implemented.');
808 }
@@ -2421,6 +2502,9 @@ if (enableCache) {
2502 (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
2503 (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
2504 }
2505 +if (enableUseHook) {
2506 + (ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
2507 +}
2508 if (enableUseMemoCacheHook) {
2509 (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
2510 }
@@ -2452,6 +2536,9 @@ if (enableCache) {
2536 (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
2537 (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
2538 }
2539 +if (enableUseHook) {
2540 + (HooksDispatcherOnMount: Dispatcher).use = use;
2541 +}
2542 if (enableUseMemoCacheHook) {
2543 (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
2544 }
@@ -2485,6 +2572,9 @@ if (enableCache) {
2572 if (enableUseMemoCacheHook) {
2573 (HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
2574 }
2575 +if (enableUseHook) {
2576 + (HooksDispatcherOnUpdate: Dispatcher).use = use;
2577 +}
2578
2579 const HooksDispatcherOnRerender: Dispatcher = {
2580 readContext,
@@ -2513,6 +2603,9 @@ if (enableCache) {
2603 (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
2604 (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
2605 }
2606 +if (enableUseHook) {
2607 + (HooksDispatcherOnRerender: Dispatcher).use = use;
2608 +}
2609 if (enableUseMemoCacheHook) {
2610 (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
2611 }
@@ -2691,6 +2784,9 @@ if (__DEV__) {
2784 return mountRefresh();
2785 };
2786 }
2787 + if (enableUseHook) {
2788 + (HooksDispatcherOnMountInDEV: Dispatcher).use = use;
2789 + }
2790 if (enableUseMemoCacheHook) {
2791 (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2792 }
@@ -2836,6 +2932,9 @@ if (__DEV__) {
2932 return mountRefresh();
2933 };
2934 }
2935 + if (enableUseHook) {
2936 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
2937 + }
2938 if (enableUseMemoCacheHook) {
2939 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
2940 }
@@ -2981,6 +3080,9 @@ if (__DEV__) {
3080 return updateRefresh();
3081 };
3082 }
3083 + if (enableUseHook) {
3084 + (HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
3085 + }
3086 if (enableUseMemoCacheHook) {
3087 (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3088 }
@@ -3127,6 +3229,9 @@ if (__DEV__) {
3229 return updateRefresh();
3230 };
3231 }
3232 + if (enableUseHook) {
3233 + (HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
3234 + }
3235 if (enableUseMemoCacheHook) {
3236 (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
3237 }
@@ -3289,6 +3394,14 @@ if (__DEV__) {
3394 return mountRefresh();
3395 };
3396 }
3397 + if (enableUseHook) {
3398 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function<T>(
3399 + usable: Usable<T>,
3400 + ): T {
3401 + warnInvalidHookAccess();
3402 + return use(usable);
3403 + };
3404 + }
3405 if (enableUseMemoCacheHook) {
3406 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
3407 size: number,
@@ -3456,6 +3569,14 @@ if (__DEV__) {
3569 return updateRefresh();
3570 };
3571 }
3572 + if (enableUseHook) {
3573 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function<T>(
3574 + usable: Usable<T>,
3575 + ): T {
3576 + warnInvalidHookAccess();
3577 + return use(usable);
3578 + };
3579 + }
3580 if (enableUseMemoCacheHook) {
3581 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
3582 size: number,
@@ -3624,6 +3745,14 @@ if (__DEV__) {
3745 return updateRefresh();
3746 };
3747 }
3748 + if (enableUseHook) {
3749 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function<T>(
3750 + usable: Usable<T>,
3751 + ): T {
3752 + warnInvalidHookAccess();
3753 + return use(usable);
3754 + };
3755 + }
3756 if (enableUseMemoCacheHook) {
3757 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
3758 size: number,
packages/react-reconciler/src/ReactFiberLane.new.js
+16 -2
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
403 // Iterate through the pending lanes and check if we've reached their
404 // expiration time. If so, we'll assume the update is being starved and mark
405 // it as expired to force it to finish.
406 - let lanes = pendingLanes;
406 + //
407 + // We exclude retry lanes because those must always be time sliced, in order
408 + // to unwrap uncached promises.
409 + // TODO: Write a test for this
410 + let lanes = pendingLanes & ~RetryLanes;
411 while (lanes > 0) {
412 const index = pickArbitraryLaneIndex(lanes);
413 const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
439 return getHighestPriorityLanes(root.pendingLanes);
440 }
441
438 -export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
442 +export function getLanesToRetrySynchronouslyOnError(
443 + root: FiberRoot,
444 + originallyAttemptedLanes: Lanes,
445 +): Lanes {
446 + if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
447 + // The error recovery mechanism is disabled until these lanes are cleared.
448 + return NoLanes;
449 + }
450 +
451 const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
452 if (everythingButOffscreen !== NoLanes) {
453 return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
658
659 root.entangledLanes &= remainingLanes;
660
661 + root.errorRecoveryDisabledLanes &= remainingLanes;
662 +
663 const entanglements = root.entanglements;
664 const eventTimes = root.eventTimes;
665 const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberLane.old.js
+16 -2
@@ -403,7 +403,11 @@ export function markStarvedLanesAsExpired(
403 // Iterate through the pending lanes and check if we've reached their
404 // expiration time. If so, we'll assume the update is being starved and mark
405 // it as expired to force it to finish.
406 - let lanes = pendingLanes;
406 + //
407 + // We exclude retry lanes because those must always be time sliced, in order
408 + // to unwrap uncached promises.
409 + // TODO: Write a test for this
410 + let lanes = pendingLanes & ~RetryLanes;
411 while (lanes > 0) {
412 const index = pickArbitraryLaneIndex(lanes);
413 const lane = 1 << index;
@@ -435,7 +439,15 @@ export function getHighestPriorityPendingLanes(root: FiberRoot) {
439 return getHighestPriorityLanes(root.pendingLanes);
440 }
441
438 -export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
442 +export function getLanesToRetrySynchronouslyOnError(
443 + root: FiberRoot,
444 + originallyAttemptedLanes: Lanes,
445 +): Lanes {
446 + if (root.errorRecoveryDisabledLanes & originallyAttemptedLanes) {
447 + // The error recovery mechanism is disabled until these lanes are cleared.
448 + return NoLanes;
449 + }
450 +
451 const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
452 if (everythingButOffscreen !== NoLanes) {
453 return everythingButOffscreen;
@@ -646,6 +658,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
658
659 root.entangledLanes &= remainingLanes;
660
661 + root.errorRecoveryDisabledLanes &= remainingLanes;
662 +
663 const entanglements = root.entanglements;
664 const eventTimes = root.eventTimes;
665 const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberRoot.new.js
+1
@@ -70,6 +70,7 @@ function FiberRootNode(
70 this.expiredLanes = NoLanes;
71 this.mutableReadLanes = NoLanes;
72 this.finishedLanes = NoLanes;
73 + this.errorRecoveryDisabledLanes = NoLanes;
74
75 this.entangledLanes = NoLanes;
76 this.entanglements = createLaneMap(NoLanes);
packages/react-reconciler/src/ReactFiberRoot.old.js
+1
@@ -70,6 +70,7 @@ function FiberRootNode(
70 this.expiredLanes = NoLanes;
71 this.mutableReadLanes = NoLanes;
72 this.finishedLanes = NoLanes;
73 + this.errorRecoveryDisabledLanes = NoLanes;
74
75 this.entangledLanes = NoLanes;
76 this.entanglements = createLaneMap(NoLanes);
packages/react-reconciler/src/ReactFiberThrow.new.js
+7 -50
@@ -57,7 +57,7 @@ import {
57 onUncaughtError,
58 markLegacyErrorBoundaryAsFailed,
59 isAlreadyFailedLegacyErrorBoundary,
60 - pingSuspendedRoot,
60 + attachPingListener,
61 restorePendingUpdaters,
62 } from './ReactFiberWorkLoop.new';
63 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
@@ -78,8 +78,6 @@ import {
78 queueHydrationError,
79 } from './ReactFiberHydrationContext.new';
80
81 -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
82 -
81 function createRootErrorUpdate(
82 fiber: Fiber,
83 errorInfo: CapturedValue<mixed>,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
157 return update;
158 }
159
162 -function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
163 - // Attach a ping listener
164 - //
165 - // The data might resolve before we have a chance to commit the fallback. Or,
166 - // in the case of a refresh, we'll never commit a fallback. So we need to
167 - // attach a listener now. When it resolves ("pings"), we can decide whether to
168 - // try rendering the tree again.
169 - //
170 - // Only attach a listener if one does not already exist for the lanes
171 - // we're currently rendering (which acts like a "thread ID" here).
172 - //
173 - // We only need to do this in concurrent mode. Legacy Suspense always
174 - // commits fallbacks synchronously, so there are no pings.
175 - let pingCache = root.pingCache;
176 - let threadIDs;
177 - if (pingCache === null) {
178 - pingCache = root.pingCache = new PossiblyWeakMap();
179 - threadIDs = new Set();
180 - pingCache.set(wakeable, threadIDs);
181 - } else {
182 - threadIDs = pingCache.get(wakeable);
183 - if (threadIDs === undefined) {
184 - threadIDs = new Set();
185 - pingCache.set(wakeable, threadIDs);
186 - }
187 - }
188 - if (!threadIDs.has(lanes)) {
189 - // Memoize using the thread ID to prevent redundant listeners.
190 - threadIDs.add(lanes);
191 - const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
192 - if (enableUpdaterTracking) {
193 - if (isDevToolsPresent) {
194 - // If we have pending work still, restore the original updaters
195 - restorePendingUpdaters(root, lanes);
196 - }
197 - }
198 - wakeable.then(ping, ping);
199 - }
200 -}
201 -
160 function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
161 if (enableLazyContextPropagation) {
162 const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
315 sourceFiber: Fiber,
316 value: mixed,
317 rootRenderLanes: Lanes,
360 -): Wakeable | null {
318 +): void {
319 // The source fiber did not complete.
320 sourceFiber.flags |= Incomplete;
321
@@ -459,7 +417,7 @@ function throwException(
417 if (suspenseBoundary.mode & ConcurrentMode) {
418 attachPingListener(root, wakeable, rootRenderLanes);
419 }
462 - return wakeable;
420 + return;
421 } else {
422 // No boundary was found. Unless this is a sync update, this is OK.
423 // We can suspend and wait for more data to arrive.
@@ -474,7 +432,7 @@ function throwException(
432 // This case also applies to initial hydration.
433 attachPingListener(root, wakeable, rootRenderLanes);
434 renderDidSuspendDelayIfPossible();
477 - return wakeable;
435 + return;
436 }
437
438 // This is a sync/discrete update. We treat this case like an error
@@ -517,7 +475,7 @@ function throwException(
475 // Even though the user may not be affected by this error, we should
476 // still log it so it can be fixed.
477 queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
520 - return null;
478 + return;
479 }
480 } else {
481 // Otherwise, fall through to the error path.
@@ -540,7 +498,7 @@ function throwException(
498 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
499 const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
500 enqueueCapturedUpdate(workInProgress, update);
543 - return null;
501 + return;
502 }
503 case ClassComponent:
504 // Capture and retry
@@ -564,7 +522,7 @@ function throwException(
522 lane,
523 );
524 enqueueCapturedUpdate(workInProgress, update);
567 - return null;
525 + return;
526 }
527 break;
528 default:
@@ -572,7 +530,6 @@ function throwException(
530 }
531 workInProgress = workInProgress.return;
532 } while (workInProgress !== null);
575 - return null;
533 }
534
535 export {throwException, createRootErrorUpdate, createClassErrorUpdate};
packages/react-reconciler/src/ReactFiberThrow.old.js
+7 -50
@@ -57,7 +57,7 @@ import {
57 onUncaughtError,
58 markLegacyErrorBoundaryAsFailed,
59 isAlreadyFailedLegacyErrorBoundary,
60 - pingSuspendedRoot,
60 + attachPingListener,
61 restorePendingUpdaters,
62 } from './ReactFiberWorkLoop.old';
63 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
@@ -78,8 +78,6 @@ import {
78 queueHydrationError,
79 } from './ReactFiberHydrationContext.old';
80
81 -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
82 -
81 function createRootErrorUpdate(
82 fiber: Fiber,
83 errorInfo: CapturedValue<mixed>,
@@ -159,46 +157,6 @@ function createClassErrorUpdate(
157 return update;
158 }
159
162 -function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
163 - // Attach a ping listener
164 - //
165 - // The data might resolve before we have a chance to commit the fallback. Or,
166 - // in the case of a refresh, we'll never commit a fallback. So we need to
167 - // attach a listener now. When it resolves ("pings"), we can decide whether to
168 - // try rendering the tree again.
169 - //
170 - // Only attach a listener if one does not already exist for the lanes
171 - // we're currently rendering (which acts like a "thread ID" here).
172 - //
173 - // We only need to do this in concurrent mode. Legacy Suspense always
174 - // commits fallbacks synchronously, so there are no pings.
175 - let pingCache = root.pingCache;
176 - let threadIDs;
177 - if (pingCache === null) {
178 - pingCache = root.pingCache = new PossiblyWeakMap();
179 - threadIDs = new Set();
180 - pingCache.set(wakeable, threadIDs);
181 - } else {
182 - threadIDs = pingCache.get(wakeable);
183 - if (threadIDs === undefined) {
184 - threadIDs = new Set();
185 - pingCache.set(wakeable, threadIDs);
186 - }
187 - }
188 - if (!threadIDs.has(lanes)) {
189 - // Memoize using the thread ID to prevent redundant listeners.
190 - threadIDs.add(lanes);
191 - const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
192 - if (enableUpdaterTracking) {
193 - if (isDevToolsPresent) {
194 - // If we have pending work still, restore the original updaters
195 - restorePendingUpdaters(root, lanes);
196 - }
197 - }
198 - wakeable.then(ping, ping);
199 - }
200 -}
201 -
160 function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
161 if (enableLazyContextPropagation) {
162 const currentSourceFiber = sourceFiber.alternate;
@@ -357,7 +315,7 @@ function throwException(
315 sourceFiber: Fiber,
316 value: mixed,
317 rootRenderLanes: Lanes,
360 -): Wakeable | null {
318 +): void {
319 // The source fiber did not complete.
320 sourceFiber.flags |= Incomplete;
321
@@ -459,7 +417,7 @@ function throwException(
417 if (suspenseBoundary.mode & ConcurrentMode) {
418 attachPingListener(root, wakeable, rootRenderLanes);
419 }
462 - return wakeable;
420 + return;
421 } else {
422 // No boundary was found. Unless this is a sync update, this is OK.
423 // We can suspend and wait for more data to arrive.
@@ -474,7 +432,7 @@ function throwException(
432 // This case also applies to initial hydration.
433 attachPingListener(root, wakeable, rootRenderLanes);
434 renderDidSuspendDelayIfPossible();
477 - return wakeable;
435 + return;
436 }
437
438 // This is a sync/discrete update. We treat this case like an error
@@ -517,7 +475,7 @@ function throwException(
475 // Even though the user may not be affected by this error, we should
476 // still log it so it can be fixed.
477 queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
520 - return null;
478 + return;
479 }
480 } else {
481 // Otherwise, fall through to the error path.
@@ -540,7 +498,7 @@ function throwException(
498 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
499 const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
500 enqueueCapturedUpdate(workInProgress, update);
543 - return null;
501 + return;
502 }
503 case ClassComponent:
504 // Capture and retry
@@ -564,7 +522,7 @@ function throwException(
522 lane,
523 );
524 enqueueCapturedUpdate(workInProgress, update);
567 - return null;
525 + return;
526 }
527 break;
528 default:
@@ -572,7 +530,6 @@ function throwException(
530 }
531 workInProgress = workInProgress.return;
532 } while (workInProgress !== null);
575 - return null;
533 }
534
535 export {throwException, createRootErrorUpdate, createClassErrorUpdate};
packages/react-reconciler/src/ReactFiberWakeable.new.js
+105 -19
@@ -7,38 +7,104 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {
11 + Wakeable,
12 + Thenable,
13 + PendingThenable,
14 + FulfilledThenable,
15 + RejectedThenable,
16 +} from 'shared/ReactTypes';
17
12 -let suspendedWakeable: Wakeable | null = null;
13 -let wasPinged = false;
18 +let suspendedThenable: Thenable<mixed> | null = null;
19 let adHocSuspendCount: number = 0;
20
21 +let usedThenables: Array<Thenable<any> | void> | null = null;
22 +let lastUsedThenable: Thenable<any> | null = null;
23 +
24 const MAX_AD_HOC_SUSPEND_COUNT = 50;
25
18 -export function suspendedWakeableWasPinged() {
19 - return wasPinged;
26 +export function isTrackingSuspendedThenable() {
27 + return suspendedThenable !== null;
28 }
29
22 -export function trackSuspendedWakeable(wakeable: Wakeable) {
23 - adHocSuspendCount++;
24 - suspendedWakeable = wakeable;
30 +export function suspendedThenableDidResolve() {
31 + if (suspendedThenable !== null) {
32 + const status = suspendedThenable.status;
33 + return status === 'fulfilled' || status === 'rejected';
34 + }
35 + return false;
36 }
37
27 -export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28 - if (wakeable === suspendedWakeable) {
29 - // This ping is from the wakeable that just suspended. Mark it as pinged.
30 - // When the work loop resumes, we'll immediately try rendering the fiber
31 - // again instead of unwinding the stack.
32 - wasPinged = true;
33 - return true;
38 +export function trackSuspendedWakeable(wakeable: Wakeable) {
39 + // If this wakeable isn't already a thenable, turn it into one now. Then,
40 + // when we resume the work loop, we can check if its status is
41 + // still pending.
42 + // TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
43 + const thenable: Thenable<mixed> = (wakeable: any);
44 +
45 + if (thenable !== lastUsedThenable) {
46 + // If this wakeable was not just `use`-d, it must be an ad hoc wakeable
47 + // that was thrown by an older Suspense implementation. Keep a count of
48 + // these so that we can detect an infinite ping loop.
49 + // TODO: Once `use` throws an opaque signal instead of the actual thenable,
50 + // a better way to count ad hoc suspends is whether an actual thenable
51 + // is caught by the work loop.
52 + adHocSuspendCount++;
53 + }
54 + suspendedThenable = thenable;
55 +
56 + // We use an expando to track the status and result of a thenable so that we
57 + // can synchronously unwrap the value. Think of this as an extension of the
58 + // Promise API, or a custom interface that is a superset of Thenable.
59 + //
60 + // If the thenable doesn't have a status, set it to "pending" and attach
61 + // a listener that will update its status and result when it resolves.
62 + switch (thenable.status) {
63 + case 'pending':
64 + // Since the status is already "pending", we can assume it will be updated
65 + // when it resolves, either by React or something in userspace.
66 + break;
67 + case 'fulfilled':
68 + case 'rejected':
69 + // A thenable that already resolved shouldn't have been thrown, so this is
70 + // unexpected. Suggests a mistake in a userspace data library. Don't track
71 + // this thenable, because if we keep trying it will likely infinite loop
72 + // without ever resolving.
73 + // TODO: Log a warning?
74 + suspendedThenable = null;
75 + break;
76 + default: {
77 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
78 + pendingThenable.status = 'pending';
79 + pendingThenable.then(
80 + fulfilledValue => {
81 + if (thenable.status === 'pending') {
82 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
83 + fulfilledThenable.status = 'fulfilled';
84 + fulfilledThenable.value = fulfilledValue;
85 + }
86 + },
87 + (error: mixed) => {
88 + if (thenable.status === 'pending') {
89 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
90 + rejectedThenable.status = 'rejected';
91 + rejectedThenable.reason = error;
92 + }
93 + },
94 + );
95 + break;
96 + }
97 }
35 - return false;
98 }
99
38 -export function resetWakeableState() {
39 - suspendedWakeable = null;
40 - wasPinged = false;
100 +export function resetWakeableStateAfterEachAttempt() {
101 + suspendedThenable = null;
102 adHocSuspendCount = 0;
103 + lastUsedThenable = null;
104 +}
105 +
106 +export function resetThenableStateOnCompletion() {
107 + usedThenables = null;
108 }
109
110 export function throwIfInfinitePingLoopDetected() {
@@ -48,3 +114,23 @@ export function throwIfInfinitePingLoopDetected() {
114 // the render phase so that it gets the component stack.
115 }
116 }
117 +
118 +export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
119 + if (usedThenables === null) {
120 + usedThenables = [];
121 + }
122 + usedThenables[index] = thenable;
123 + lastUsedThenable = thenable;
124 +}
125 +
126 +export function getPreviouslyUsedThenableAtIndex<T>(
127 + index: number,
128 +): Thenable<T> | null {
129 + if (usedThenables !== null) {
130 + const thenable = usedThenables[index];
131 + if (thenable !== undefined) {
132 + return thenable;
133 + }
134 + }
135 + return null;
136 +}
packages/react-reconciler/src/ReactFiberWakeable.old.js
+105 -19
@@ -7,38 +7,104 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {
11 + Wakeable,
12 + Thenable,
13 + PendingThenable,
14 + FulfilledThenable,
15 + RejectedThenable,
16 +} from 'shared/ReactTypes';
17
12 -let suspendedWakeable: Wakeable | null = null;
13 -let wasPinged = false;
18 +let suspendedThenable: Thenable<mixed> | null = null;
19 let adHocSuspendCount: number = 0;
20
21 +let usedThenables: Array<Thenable<any> | void> | null = null;
22 +let lastUsedThenable: Thenable<any> | null = null;
23 +
24 const MAX_AD_HOC_SUSPEND_COUNT = 50;
25
18 -export function suspendedWakeableWasPinged() {
19 - return wasPinged;
26 +export function isTrackingSuspendedThenable() {
27 + return suspendedThenable !== null;
28 }
29
22 -export function trackSuspendedWakeable(wakeable: Wakeable) {
23 - adHocSuspendCount++;
24 - suspendedWakeable = wakeable;
30 +export function suspendedThenableDidResolve() {
31 + if (suspendedThenable !== null) {
32 + const status = suspendedThenable.status;
33 + return status === 'fulfilled' || status === 'rejected';
34 + }
35 + return false;
36 }
37
27 -export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28 - if (wakeable === suspendedWakeable) {
29 - // This ping is from the wakeable that just suspended. Mark it as pinged.
30 - // When the work loop resumes, we'll immediately try rendering the fiber
31 - // again instead of unwinding the stack.
32 - wasPinged = true;
33 - return true;
38 +export function trackSuspendedWakeable(wakeable: Wakeable) {
39 + // If this wakeable isn't already a thenable, turn it into one now. Then,
40 + // when we resume the work loop, we can check if its status is
41 + // still pending.
42 + // TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
43 + const thenable: Thenable<mixed> = (wakeable: any);
44 +
45 + if (thenable !== lastUsedThenable) {
46 + // If this wakeable was not just `use`-d, it must be an ad hoc wakeable
47 + // that was thrown by an older Suspense implementation. Keep a count of
48 + // these so that we can detect an infinite ping loop.
49 + // TODO: Once `use` throws an opaque signal instead of the actual thenable,
50 + // a better way to count ad hoc suspends is whether an actual thenable
51 + // is caught by the work loop.
52 + adHocSuspendCount++;
53 + }
54 + suspendedThenable = thenable;
55 +
56 + // We use an expando to track the status and result of a thenable so that we
57 + // can synchronously unwrap the value. Think of this as an extension of the
58 + // Promise API, or a custom interface that is a superset of Thenable.
59 + //
60 + // If the thenable doesn't have a status, set it to "pending" and attach
61 + // a listener that will update its status and result when it resolves.
62 + switch (thenable.status) {
63 + case 'pending':
64 + // Since the status is already "pending", we can assume it will be updated
65 + // when it resolves, either by React or something in userspace.
66 + break;
67 + case 'fulfilled':
68 + case 'rejected':
69 + // A thenable that already resolved shouldn't have been thrown, so this is
70 + // unexpected. Suggests a mistake in a userspace data library. Don't track
71 + // this thenable, because if we keep trying it will likely infinite loop
72 + // without ever resolving.
73 + // TODO: Log a warning?
74 + suspendedThenable = null;
75 + break;
76 + default: {
77 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
78 + pendingThenable.status = 'pending';
79 + pendingThenable.then(
80 + fulfilledValue => {
81 + if (thenable.status === 'pending') {
82 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
83 + fulfilledThenable.status = 'fulfilled';
84 + fulfilledThenable.value = fulfilledValue;
85 + }
86 + },
87 + (error: mixed) => {
88 + if (thenable.status === 'pending') {
89 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
90 + rejectedThenable.status = 'rejected';
91 + rejectedThenable.reason = error;
92 + }
93 + },
94 + );
95 + break;
96 + }
97 }
35 - return false;
98 }
99
38 -export function resetWakeableState() {
39 - suspendedWakeable = null;
40 - wasPinged = false;
100 +export function resetWakeableStateAfterEachAttempt() {
101 + suspendedThenable = null;
102 adHocSuspendCount = 0;
103 + lastUsedThenable = null;
104 +}
105 +
106 +export function resetThenableStateOnCompletion() {
107 + usedThenables = null;
108 }
109
110 export function throwIfInfinitePingLoopDetected() {
@@ -48,3 +114,23 @@ export function throwIfInfinitePingLoopDetected() {
114 // the render phase so that it gets the component stack.
115 }
116 }
117 +
118 +export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
119 + if (usedThenables === null) {
120 + usedThenables = [];
121 + }
122 + usedThenables[index] = thenable;
123 + lastUsedThenable = thenable;
124 +}
125 +
126 +export function getPreviouslyUsedThenableAtIndex<T>(
127 + index: number,
128 +): Thenable<T> | null {
129 + if (usedThenables !== null) {
130 + const thenable = usedThenables[index];
131 + if (thenable !== undefined) {
132 + return thenable;
133 + }
134 + }
135 + return null;
136 +}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+267 -135
@@ -254,14 +254,17 @@ import {
254 } from './ReactFiberAct.new';
255 import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
256 import {
257 - resetWakeableState,
257 + resetWakeableStateAfterEachAttempt,
258 + resetThenableStateOnCompletion,
259 trackSuspendedWakeable,
259 - suspendedWakeableWasPinged,
260 - attemptToPingSuspendedWakeable,
260 + suspendedThenableDidResolve,
261 + isTrackingSuspendedThenable,
262 } from './ReactFiberWakeable.new';
263
264 const ceil = Math.ceil;
265
266 +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
267 +
268 const {
269 ReactCurrentDispatcher,
270 ReactCurrentOwner,
@@ -299,6 +302,12 @@ let workInProgressRootRenderLanes: Lanes = NoLanes;
302 // after this happens. If the fiber is pinged before we resume, we can retry
303 // immediately instead of unwinding the stack.
304 let workInProgressIsSuspended: boolean = false;
305 +let workInProgressThrownValue: mixed = null;
306 +
307 +// Whether a ping listener was attached during this render. This is slightly
308 +// different that whether something suspended, because we don't add multiple
309 +// listeners to a promise we've already seen (per root and lane).
310 +let workInProgressRootDidAttachPingListener: boolean = false;
311
312 // A contextual version of workInProgressRootRenderLanes. It is a superset of
313 // the lanes that we started working on at the root. When we enter a subtree
@@ -1013,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1022 // render synchronously to block concurrent data mutations, and we'll
1023 // includes all pending updates are included. If it still fails after
1024 // the second attempt, we'll give up and commit the resulting tree.
1016 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1025 + const originallyAttemptedLanes = lanes;
1026 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1027 + root,
1028 + originallyAttemptedLanes,
1029 + );
1030 if (errorRetryLanes !== NoLanes) {
1031 lanes = errorRetryLanes;
1019 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1032 + exitStatus = recoverFromConcurrentError(
1033 + root,
1034 + originallyAttemptedLanes,
1035 + errorRetryLanes,
1036 + );
1037 }
1038 }
1039 if (exitStatus === RootFatalErrored) {
@@ -1056,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1073
1074 // We need to check again if something threw
1075 if (exitStatus === RootErrored) {
1059 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1076 + const originallyAttemptedLanes = lanes;
1077 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1078 + root,
1079 + originallyAttemptedLanes,
1080 + );
1081 if (errorRetryLanes !== NoLanes) {
1082 lanes = errorRetryLanes;
1062 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1083 + exitStatus = recoverFromConcurrentError(
1084 + root,
1085 + originallyAttemptedLanes,
1086 + errorRetryLanes,
1087 + );
1088 // We assume the tree is now consistent because we didn't yield to any
1089 // concurrent events.
1090 }
@@ -1090,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1115 return null;
1116 }
1117
1093 -function recoverFromConcurrentError(root, errorRetryLanes) {
1118 +function recoverFromConcurrentError(
1119 + root,
1120 + originallyAttemptedLanes,
1121 + errorRetryLanes,
1122 +) {
1123 // If an error occurred during hydration, discard server response and fall
1124 // back to client side render.
1125
1126 // Before rendering again, save the errors from the previous attempt.
1127 const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1128
1100 - if (isRootDehydrated(root)) {
1129 + const wasRootDehydrated = isRootDehydrated(root);
1130 + if (wasRootDehydrated) {
1131 // The shell failed to hydrate. Set a flag to force a client rendering
1132 // during the next attempt. To do this, we call prepareFreshStack now
1133 // to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1120,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
1150 if (exitStatus !== RootErrored) {
1151 // Successfully finished rendering on retry
1152
1153 + if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
1154 + // During the synchronous render, we attached additional ping listeners.
1155 + // This is highly suggestive of an uncached promise (though it's not the
1156 + // only reason this would happen). If it was an uncached promise, then
1157 + // it may have masked a downstream error from ocurring without actually
1158 + // fixing it. Example:
1159 + //
1160 + // use(Promise.resolve('uncached'))
1161 + // throw new Error('Oops!')
1162 + //
1163 + // When this happens, there's a conflict between blocking potential
1164 + // concurrent data races and unwrapping uncached promise values. We
1165 + // have to choose one or the other. Because the data race recovery is
1166 + // a last ditch effort, we'll disable it.
1167 + root.errorRecoveryDisabledLanes = mergeLanes(
1168 + root.errorRecoveryDisabledLanes,
1169 + originallyAttemptedLanes,
1170 + );
1171 +
1172 + // Mark the current render as suspended and force it to restart. Once
1173 + // these lanes finish successfully, we'll re-enable the error recovery
1174 + // mechanism for subsequent updates.
1175 + workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
1176 + return RootSuspendedWithDelay;
1177 + }
1178 +
1179 // The errors from the failed first attempt have been recovered. Add
1180 // them to the collection of recoverable errors. We'll log them in the
1181 // commit phase.
@@ -1376,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
1432 // synchronously to block concurrent data mutations, and we'll includes
1433 // all pending updates are included. If it still fails after the second
1434 // attempt, we'll give up and commit the resulting tree.
1379 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1435 + const originallyAttemptedLanes = lanes;
1436 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1437 + root,
1438 + originallyAttemptedLanes,
1439 + );
1440 if (errorRetryLanes !== NoLanes) {
1441 lanes = errorRetryLanes;
1382 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1442 + exitStatus = recoverFromConcurrentError(
1443 + root,
1444 + originallyAttemptedLanes,
1445 + errorRetryLanes,
1446 + );
1447 }
1448 }
1449
@@ -1596,13 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1660 );
1661 interruptedWork = interruptedWork.return;
1662 }
1599 - resetWakeableState();
1663 + resetWakeableStateAfterEachAttempt();
1664 + resetThenableStateOnCompletion();
1665 }
1666 workInProgressRoot = root;
1667 const rootWorkInProgress = createWorkInProgress(root.current, null);
1668 workInProgress = rootWorkInProgress;
1669 workInProgressRootRenderLanes = renderLanes = lanes;
1670 workInProgressIsSuspended = false;
1671 + workInProgressThrownValue = null;
1672 + workInProgressRootDidAttachPingListener = false;
1673 workInProgressRootExitStatus = RootInProgress;
1674 workInProgressRootFatalError = null;
1675 workInProgressRootSkippedLanes = NoLanes;
@@ -1621,94 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1688 return rootWorkInProgress;
1689 }
1690
1624 -function handleError(root, thrownValue): Wakeable | null {
1625 - do {
1626 - let erroredWork = workInProgress;
1627 - try {
1628 - // Reset module-level state that was set during the render phase.
1629 - resetContextDependencies();
1630 - resetHooksAfterThrow();
1631 - resetCurrentDebugFiberInDEV();
1632 - // TODO: I found and added this missing line while investigating a
1633 - // separate issue. Write a regression test using string refs.
1634 - ReactCurrentOwner.current = null;
1635 -
1636 - if (erroredWork === null || erroredWork.return === null) {
1637 - // Expected to be working on a non-root fiber. This is a fatal error
1638 - // because there's no ancestor that can handle it; the root is
1639 - // supposed to capture all errors that weren't caught by an error
1640 - // boundary.
1641 - workInProgressRootExitStatus = RootFatalErrored;
1642 - workInProgressRootFatalError = thrownValue;
1643 - // Set `workInProgress` to null. This represents advancing to the next
1644 - // sibling, or the parent if there are no siblings. But since the root
1645 - // has no siblings nor a parent, we set it to null. Usually this is
1646 - // handled by `completeUnitOfWork` or `unwindWork`, but since we're
1647 - // intentionally not calling those, we need set it here.
1648 - // TODO: Consider calling `unwindWork` to pop the contexts.
1649 - workInProgress = null;
1650 - return null;
1651 - }
1691 +function handleThrow(root, thrownValue): void {
1692 + // Reset module-level state that was set during the render phase.
1693 + resetContextDependencies();
1694 + resetHooksAfterThrow();
1695 + resetCurrentDebugFiberInDEV();
1696 + // TODO: I found and added this missing line while investigating a
1697 + // separate issue. Write a regression test using string refs.
1698 + ReactCurrentOwner.current = null;
1699
1653 - if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1654 - // Record the time spent rendering before an error was thrown. This
1655 - // avoids inaccurate Profiler durations in the case of a
1656 - // suspended render.
1657 - stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1658 - }
1700 + // Setting this to `true` tells the work loop to unwind the stack instead
1701 + // of entering the begin phase. It's called "suspended" because it usually
1702 + // happens because of Suspense, but it also applies to errors. Think of it
1703 + // as suspending the execution of the work loop.
1704 + workInProgressIsSuspended = true;
1705 + workInProgressThrownValue = thrownValue;
1706 +
1707 + const erroredWork = workInProgress;
1708 + if (erroredWork === null) {
1709 + // This is a fatal error
1710 + workInProgressRootExitStatus = RootFatalErrored;
1711 + workInProgressRootFatalError = thrownValue;
1712 + return;
1713 + }
1714
1660 - if (enableSchedulingProfiler) {
1661 - markComponentRenderStopped();
1715 + const isWakeable =
1716 + thrownValue !== null &&
1717 + typeof thrownValue === 'object' &&
1718 + typeof thrownValue.then === 'function';
1719
1663 - if (
1664 - thrownValue !== null &&
1665 - typeof thrownValue === 'object' &&
1666 - typeof thrownValue.then === 'function'
1667 - ) {
1668 - const wakeable: Wakeable = (thrownValue: any);
1669 - markComponentSuspended(
1670 - erroredWork,
1671 - wakeable,
1672 - workInProgressRootRenderLanes,
1673 - );
1674 - } else {
1675 - markComponentErrored(
1676 - erroredWork,
1677 - thrownValue,
1678 - workInProgressRootRenderLanes,
1679 - );
1680 - }
1681 - }
1720 + if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1721 + // Record the time spent rendering before an error was thrown. This
1722 + // avoids inaccurate Profiler durations in the case of a
1723 + // suspended render.
1724 + stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1725 + }
1726
1683 - const maybeWakeable = throwException(
1684 - root,
1685 - erroredWork.return,
1727 + if (enableSchedulingProfiler) {
1728 + markComponentRenderStopped();
1729 + if (isWakeable) {
1730 + const wakeable: Wakeable = (thrownValue: any);
1731 + markComponentSuspended(
1732 + erroredWork,
1733 + wakeable,
1734 + workInProgressRootRenderLanes,
1735 + );
1736 + } else {
1737 + markComponentErrored(
1738 erroredWork,
1739 thrownValue,
1740 workInProgressRootRenderLanes,
1741 );
1690 - // Setting this to `true` tells the work loop to unwind the stack instead
1691 - // of entering the begin phase. It's called "suspended" because it usually
1692 - // happens because of Suspense, but it also applies to errors. Think of it
1693 - // as suspending the execution of the work loop.
1694 - workInProgressIsSuspended = true;
1695 -
1696 - // Return to the normal work loop.
1697 - return maybeWakeable;
1698 - } catch (yetAnotherThrownValue) {
1699 - // Something in the return path also threw.
1700 - thrownValue = yetAnotherThrownValue;
1701 - if (workInProgress === erroredWork && erroredWork !== null) {
1702 - // If this boundary has already errored, then we had trouble processing
1703 - // the error. Bubble it to the next boundary.
1704 - erroredWork = erroredWork.return;
1705 - workInProgress = erroredWork;
1706 - } else {
1707 - erroredWork = workInProgress;
1708 - }
1709 - continue;
1742 }
1711 - } while (true);
1743 + }
1744 +
1745 + if (isWakeable) {
1746 + const wakeable: Wakeable = (thrownValue: any);
1747 +
1748 + trackSuspendedWakeable(wakeable);
1749 + }
1750 }
1751
1752 function pushDispatcher() {
@@ -1834,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1872 workLoopSync();
1873 break;
1874 } catch (thrownValue) {
1837 - handleError(root, thrownValue);
1875 + handleThrow(root, thrownValue);
1876 }
1877 } while (true);
1878 resetContextDependencies();
@@ -1872,10 +1910,15 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1910 function workLoopSync() {
1911 // Perform work without checking if we need to yield between fiber.
1912
1875 - if (workInProgressIsSuspended && workInProgress !== null) {
1913 + if (workInProgressIsSuspended) {
1914 // The current work-in-progress was already attempted. We need to unwind
1915 // it before we continue the normal work loop.
1878 - resumeSuspendedUnitOfWork(workInProgress);
1916 + const thrownValue = workInProgressThrownValue;
1917 + workInProgressIsSuspended = false;
1918 + workInProgressThrownValue = null;
1919 + if (workInProgress !== null) {
1920 + resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1921 + }
1922 }
1923
1924 while (workInProgress !== null) {
@@ -1927,12 +1970,11 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1970 workLoopConcurrent();
1971 break;
1972 } catch (thrownValue) {
1930 - const maybeWakeable = handleError(root, thrownValue);
1931 - if (maybeWakeable !== null) {
1973 + handleThrow(root, thrownValue);
1974 + if (isTrackingSuspendedThenable()) {
1975 // If this fiber just suspended, it's possible the data is already
1976 // cached. Yield to the the main thread to give it a chance to ping. If
1977 // it does, we can retry immediately without unwinding the stack.
1935 - trackSuspendedWakeable(maybeWakeable);
1978 break;
1979 }
1980 }
@@ -1974,10 +2016,15 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2016 function workLoopConcurrent() {
2017 // Perform work until Scheduler asks us to yield
2018
1977 - if (workInProgressIsSuspended && workInProgress !== null) {
2019 + if (workInProgressIsSuspended) {
2020 // The current work-in-progress was already attempted. We need to unwind
2021 // it before we continue the normal work loop.
1980 - resumeSuspendedUnitOfWork(workInProgress);
2022 + const thrownValue = workInProgressThrownValue;
2023 + workInProgressIsSuspended = false;
2024 + workInProgressThrownValue = null;
2025 + if (workInProgress !== null) {
2026 + resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2027 + }
2028 }
2029
2030 while (workInProgress !== null && !shouldYield()) {
@@ -2013,27 +2060,72 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2060 ReactCurrentOwner.current = null;
2061 }
2062
2016 -function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
2063 +function resumeSuspendedUnitOfWork(
2064 + unitOfWork: Fiber,
2065 + thrownValue: mixed,
2066 +): void {
2067 // This is a fork of performUnitOfWork specifcally for resuming a fiber that
2068 // just suspended. In some cases, we may choose to retry the fiber immediately
2069 // instead of unwinding the stack. It's a separate function to keep the
2070 // additional logic out of the work loop's hot path.
2071
2022 - if (!suspendedWakeableWasPinged()) {
2023 - // The wakeable wasn't pinged. Return to the normal work loop. This will
2072 + const wasPinged = suspendedThenableDidResolve();
2073 + resetWakeableStateAfterEachAttempt();
2074 +
2075 + if (!wasPinged) {
2076 + // The thenable wasn't pinged. Return to the normal work loop. This will
2077 // unwind the stack, and potentially result in showing a fallback.
2025 - workInProgressIsSuspended = false;
2026 - resetWakeableState();
2078 + resetThenableStateOnCompletion();
2079 +
2080 + const returnFiber = unitOfWork.return;
2081 + if (returnFiber === null || workInProgressRoot === null) {
2082 + // Expected to be working on a non-root fiber. This is a fatal error
2083 + // because there's no ancestor that can handle it; the root is
2084 + // supposed to capture all errors that weren't caught by an error
2085 + // boundary.
2086 + workInProgressRootExitStatus = RootFatalErrored;
2087 + workInProgressRootFatalError = thrownValue;
2088 + // Set `workInProgress` to null. This represents advancing to the next
2089 + // sibling, or the parent if there are no siblings. But since the root
2090 + // has no siblings nor a parent, we set it to null. Usually this is
2091 + // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2092 + // intentionally not calling those, we need set it here.
2093 + // TODO: Consider calling `unwindWork` to pop the contexts.
2094 + workInProgress = null;
2095 + return;
2096 + }
2097 +
2098 + try {
2099 + // Find and mark the nearest Suspense or error boundary that can handle
2100 + // this "exception".
2101 + throwException(
2102 + workInProgressRoot,
2103 + returnFiber,
2104 + unitOfWork,
2105 + thrownValue,
2106 + workInProgressRootRenderLanes,
2107 + );
2108 + } catch (error) {
2109 + // We had trouble processing the error. An example of this happening is
2110 + // when accessing the `componentDidCatch` property of an error boundary
2111 + // throws an error. A weird edge case. There's a regression test for this.
2112 + // To prevent an infinite loop, bubble the error up to the next parent.
2113 + workInProgress = returnFiber;
2114 + throw error;
2115 + }
2116 +
2117 + // Return to the normal work loop.
2118 completeUnitOfWork(unitOfWork);
2119 return;
2120 }
2121
2122 // The work-in-progress was immediately pinged. Instead of unwinding the
2032 - // stack and potentially showing a fallback, reset the fiber and try rendering
2033 - // it again.
2123 + // stack and potentially showing a fallback, unwind only the last stack frame,
2124 + // reset the fiber, and try rendering it again.
2125 + const current = unitOfWork.alternate;
2126 + unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2127 unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
2128
2036 - const current = unitOfWork.alternate;
2129 setCurrentDebugFiberInDEV(unitOfWork);
2130
2131 let next;
@@ -2048,8 +2140,7 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
2140 // The begin phase finished successfully without suspending. Reset the state
2141 // used to track the fiber while it was suspended. Then return to the normal
2142 // work loop.
2051 - workInProgressIsSuspended = false;
2052 - resetWakeableState();
2143 + resetThenableStateOnCompletion();
2144
2145 resetCurrentDebugFiberInDEV();
2146 unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2850,7 +2941,53 @@ export function captureCommitPhaseError(
2941 }
2942 }
2943
2853 -export function pingSuspendedRoot(
2944 +export function attachPingListener(
2945 + root: FiberRoot,
2946 + wakeable: Wakeable,
2947 + lanes: Lanes,
2948 +) {
2949 + // Attach a ping listener
2950 + //
2951 + // The data might resolve before we have a chance to commit the fallback. Or,
2952 + // in the case of a refresh, we'll never commit a fallback. So we need to
2953 + // attach a listener now. When it resolves ("pings"), we can decide whether to
2954 + // try rendering the tree again.
2955 + //
2956 + // Only attach a listener if one does not already exist for the lanes
2957 + // we're currently rendering (which acts like a "thread ID" here).
2958 + //
2959 + // We only need to do this in concurrent mode. Legacy Suspense always
2960 + // commits fallbacks synchronously, so there are no pings.
2961 + let pingCache = root.pingCache;
2962 + let threadIDs;
2963 + if (pingCache === null) {
2964 + pingCache = root.pingCache = new PossiblyWeakMap();
2965 + threadIDs = new Set();
2966 + pingCache.set(wakeable, threadIDs);
2967 + } else {
2968 + threadIDs = pingCache.get(wakeable);
2969 + if (threadIDs === undefined) {
2970 + threadIDs = new Set();
2971 + pingCache.set(wakeable, threadIDs);
2972 + }
2973 + }
2974 + if (!threadIDs.has(lanes)) {
2975 + workInProgressRootDidAttachPingListener = true;
2976 +
2977 + // Memoize using the thread ID to prevent redundant listeners.
2978 + threadIDs.add(lanes);
2979 + const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
2980 + if (enableUpdaterTracking) {
2981 + if (isDevToolsPresent) {
2982 + // If we have pending work still, restore the original updaters
2983 + restorePendingUpdaters(root, lanes);
2984 + }
2985 + }
2986 + wakeable.then(ping, ping);
2987 + }
2988 +}
2989 +
2990 +function pingSuspendedRoot(
2991 root: FiberRoot,
2992 wakeable: Wakeable,
2993 pingedLanes: Lanes,
@@ -2874,31 +3011,26 @@ export function pingSuspendedRoot(
3011 // Received a ping at the same priority level at which we're currently
3012 // rendering. We might want to restart this render. This should mirror
3013 // the logic of whether or not a root suspends once it completes.
2877 - const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2878 - if (didPingSuspendedWakeable) {
2879 - // Successfully pinged the in-progress fiber. Don't unwind the stack.
2880 - } else {
2881 - // TODO: If we're rendering sync either due to Sync, Batched or expired,
2882 - // we should probably never restart.
3014 + // TODO: If we're rendering sync either due to Sync, Batched or expired,
3015 + // we should probably never restart.
3016
2884 - // If we're suspended with delay, or if it's a retry, we'll always suspend
2885 - // so we can always restart.
2886 - if (
2887 - workInProgressRootExitStatus === RootSuspendedWithDelay ||
2888 - (workInProgressRootExitStatus === RootSuspended &&
2889 - includesOnlyRetries(workInProgressRootRenderLanes) &&
2890 - now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2891 - ) {
2892 - // Restart from the root.
2893 - prepareFreshStack(root, NoLanes);
2894 - } else {
2895 - // Even though we can't restart right now, we might get an
2896 - // opportunity later. So we mark this render as having a ping.
2897 - workInProgressRootPingedLanes = mergeLanes(
2898 - workInProgressRootPingedLanes,
2899 - pingedLanes,
2900 - );
2901 - }
3017 + // If we're suspended with delay, or if it's a retry, we'll always suspend
3018 + // so we can always restart.
3019 + if (
3020 + workInProgressRootExitStatus === RootSuspendedWithDelay ||
3021 + (workInProgressRootExitStatus === RootSuspended &&
3022 + includesOnlyRetries(workInProgressRootRenderLanes) &&
3023 + now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
3024 + ) {
3025 + // Restart from the root.
3026 + prepareFreshStack(root, NoLanes);
3027 + } else {
3028 + // Even though we can't restart right now, we might get an
3029 + // opportunity later. So we mark this render as having a ping.
3030 + workInProgressRootPingedLanes = mergeLanes(
3031 + workInProgressRootPingedLanes,
3032 + pingedLanes,
3033 + );
3034 }
3035 }
3036
@@ -3172,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
3304 throw originalError;
3305 }
3306
3175 - // Keep this code in sync with handleError; any changes here must have
3307 + // Keep this code in sync with handleThrow; any changes here must have
3308 // corresponding changes there.
3309 resetContextDependencies();
3310 resetHooksAfterThrow();
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+267 -135
@@ -254,14 +254,17 @@ import {
254 } from './ReactFiberAct.old';
255 import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
256 import {
257 - resetWakeableState,
257 + resetWakeableStateAfterEachAttempt,
258 + resetThenableStateOnCompletion,
259 trackSuspendedWakeable,
259 - suspendedWakeableWasPinged,
260 - attemptToPingSuspendedWakeable,
260 + suspendedThenableDidResolve,
261 + isTrackingSuspendedThenable,
262 } from './ReactFiberWakeable.old';
263
264 const ceil = Math.ceil;
265
266 +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
267 +
268 const {
269 ReactCurrentDispatcher,
270 ReactCurrentOwner,
@@ -299,6 +302,12 @@ let workInProgressRootRenderLanes: Lanes = NoLanes;
302 // after this happens. If the fiber is pinged before we resume, we can retry
303 // immediately instead of unwinding the stack.
304 let workInProgressIsSuspended: boolean = false;
305 +let workInProgressThrownValue: mixed = null;
306 +
307 +// Whether a ping listener was attached during this render. This is slightly
308 +// different that whether something suspended, because we don't add multiple
309 +// listeners to a promise we've already seen (per root and lane).
310 +let workInProgressRootDidAttachPingListener: boolean = false;
311
312 // A contextual version of workInProgressRootRenderLanes. It is a superset of
313 // the lanes that we started working on at the root. When we enter a subtree
@@ -1013,10 +1022,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1022 // render synchronously to block concurrent data mutations, and we'll
1023 // includes all pending updates are included. If it still fails after
1024 // the second attempt, we'll give up and commit the resulting tree.
1016 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1025 + const originallyAttemptedLanes = lanes;
1026 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1027 + root,
1028 + originallyAttemptedLanes,
1029 + );
1030 if (errorRetryLanes !== NoLanes) {
1031 lanes = errorRetryLanes;
1019 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1032 + exitStatus = recoverFromConcurrentError(
1033 + root,
1034 + originallyAttemptedLanes,
1035 + errorRetryLanes,
1036 + );
1037 }
1038 }
1039 if (exitStatus === RootFatalErrored) {
@@ -1056,10 +1073,18 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1073
1074 // We need to check again if something threw
1075 if (exitStatus === RootErrored) {
1059 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1076 + const originallyAttemptedLanes = lanes;
1077 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1078 + root,
1079 + originallyAttemptedLanes,
1080 + );
1081 if (errorRetryLanes !== NoLanes) {
1082 lanes = errorRetryLanes;
1062 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1083 + exitStatus = recoverFromConcurrentError(
1084 + root,
1085 + originallyAttemptedLanes,
1086 + errorRetryLanes,
1087 + );
1088 // We assume the tree is now consistent because we didn't yield to any
1089 // concurrent events.
1090 }
@@ -1090,14 +1115,19 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
1115 return null;
1116 }
1117
1093 -function recoverFromConcurrentError(root, errorRetryLanes) {
1118 +function recoverFromConcurrentError(
1119 + root,
1120 + originallyAttemptedLanes,
1121 + errorRetryLanes,
1122 +) {
1123 // If an error occurred during hydration, discard server response and fall
1124 // back to client side render.
1125
1126 // Before rendering again, save the errors from the previous attempt.
1127 const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1128
1100 - if (isRootDehydrated(root)) {
1129 + const wasRootDehydrated = isRootDehydrated(root);
1130 + if (wasRootDehydrated) {
1131 // The shell failed to hydrate. Set a flag to force a client rendering
1132 // during the next attempt. To do this, we call prepareFreshStack now
1133 // to create the root work-in-progress fiber. This is a bit weird in terms
@@ -1120,6 +1150,32 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
1150 if (exitStatus !== RootErrored) {
1151 // Successfully finished rendering on retry
1152
1153 + if (workInProgressRootDidAttachPingListener && !wasRootDehydrated) {
1154 + // During the synchronous render, we attached additional ping listeners.
1155 + // This is highly suggestive of an uncached promise (though it's not the
1156 + // only reason this would happen). If it was an uncached promise, then
1157 + // it may have masked a downstream error from ocurring without actually
1158 + // fixing it. Example:
1159 + //
1160 + // use(Promise.resolve('uncached'))
1161 + // throw new Error('Oops!')
1162 + //
1163 + // When this happens, there's a conflict between blocking potential
1164 + // concurrent data races and unwrapping uncached promise values. We
1165 + // have to choose one or the other. Because the data race recovery is
1166 + // a last ditch effort, we'll disable it.
1167 + root.errorRecoveryDisabledLanes = mergeLanes(
1168 + root.errorRecoveryDisabledLanes,
1169 + originallyAttemptedLanes,
1170 + );
1171 +
1172 + // Mark the current render as suspended and force it to restart. Once
1173 + // these lanes finish successfully, we'll re-enable the error recovery
1174 + // mechanism for subsequent updates.
1175 + workInProgressRootInterleavedUpdatedLanes |= originallyAttemptedLanes;
1176 + return RootSuspendedWithDelay;
1177 + }
1178 +
1179 // The errors from the failed first attempt have been recovered. Add
1180 // them to the collection of recoverable errors. We'll log them in the
1181 // commit phase.
@@ -1376,10 +1432,18 @@ function performSyncWorkOnRoot(root) {
1432 // synchronously to block concurrent data mutations, and we'll includes
1433 // all pending updates are included. If it still fails after the second
1434 // attempt, we'll give up and commit the resulting tree.
1379 - const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
1435 + const originallyAttemptedLanes = lanes;
1436 + const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1437 + root,
1438 + originallyAttemptedLanes,
1439 + );
1440 if (errorRetryLanes !== NoLanes) {
1441 lanes = errorRetryLanes;
1382 - exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
1442 + exitStatus = recoverFromConcurrentError(
1443 + root,
1444 + originallyAttemptedLanes,
1445 + errorRetryLanes,
1446 + );
1447 }
1448 }
1449
@@ -1596,13 +1660,16 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1660 );
1661 interruptedWork = interruptedWork.return;
1662 }
1599 - resetWakeableState();
1663 + resetWakeableStateAfterEachAttempt();
1664 + resetThenableStateOnCompletion();
1665 }
1666 workInProgressRoot = root;
1667 const rootWorkInProgress = createWorkInProgress(root.current, null);
1668 workInProgress = rootWorkInProgress;
1669 workInProgressRootRenderLanes = renderLanes = lanes;
1670 workInProgressIsSuspended = false;
1671 + workInProgressThrownValue = null;
1672 + workInProgressRootDidAttachPingListener = false;
1673 workInProgressRootExitStatus = RootInProgress;
1674 workInProgressRootFatalError = null;
1675 workInProgressRootSkippedLanes = NoLanes;
@@ -1621,94 +1688,65 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1688 return rootWorkInProgress;
1689 }
1690
1624 -function handleError(root, thrownValue): Wakeable | null {
1625 - do {
1626 - let erroredWork = workInProgress;
1627 - try {
1628 - // Reset module-level state that was set during the render phase.
1629 - resetContextDependencies();
1630 - resetHooksAfterThrow();
1631 - resetCurrentDebugFiberInDEV();
1632 - // TODO: I found and added this missing line while investigating a
1633 - // separate issue. Write a regression test using string refs.
1634 - ReactCurrentOwner.current = null;
1635 -
1636 - if (erroredWork === null || erroredWork.return === null) {
1637 - // Expected to be working on a non-root fiber. This is a fatal error
1638 - // because there's no ancestor that can handle it; the root is
1639 - // supposed to capture all errors that weren't caught by an error
1640 - // boundary.
1641 - workInProgressRootExitStatus = RootFatalErrored;
1642 - workInProgressRootFatalError = thrownValue;
1643 - // Set `workInProgress` to null. This represents advancing to the next
1644 - // sibling, or the parent if there are no siblings. But since the root
1645 - // has no siblings nor a parent, we set it to null. Usually this is
1646 - // handled by `completeUnitOfWork` or `unwindWork`, but since we're
1647 - // intentionally not calling those, we need set it here.
1648 - // TODO: Consider calling `unwindWork` to pop the contexts.
1649 - workInProgress = null;
1650 - return null;
1651 - }
1691 +function handleThrow(root, thrownValue): void {
1692 + // Reset module-level state that was set during the render phase.
1693 + resetContextDependencies();
1694 + resetHooksAfterThrow();
1695 + resetCurrentDebugFiberInDEV();
1696 + // TODO: I found and added this missing line while investigating a
1697 + // separate issue. Write a regression test using string refs.
1698 + ReactCurrentOwner.current = null;
1699
1653 - if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1654 - // Record the time spent rendering before an error was thrown. This
1655 - // avoids inaccurate Profiler durations in the case of a
1656 - // suspended render.
1657 - stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1658 - }
1700 + // Setting this to `true` tells the work loop to unwind the stack instead
1701 + // of entering the begin phase. It's called "suspended" because it usually
1702 + // happens because of Suspense, but it also applies to errors. Think of it
1703 + // as suspending the execution of the work loop.
1704 + workInProgressIsSuspended = true;
1705 + workInProgressThrownValue = thrownValue;
1706 +
1707 + const erroredWork = workInProgress;
1708 + if (erroredWork === null) {
1709 + // This is a fatal error
1710 + workInProgressRootExitStatus = RootFatalErrored;
1711 + workInProgressRootFatalError = thrownValue;
1712 + return;
1713 + }
1714
1660 - if (enableSchedulingProfiler) {
1661 - markComponentRenderStopped();
1715 + const isWakeable =
1716 + thrownValue !== null &&
1717 + typeof thrownValue === 'object' &&
1718 + typeof thrownValue.then === 'function';
1719
1663 - if (
1664 - thrownValue !== null &&
1665 - typeof thrownValue === 'object' &&
1666 - typeof thrownValue.then === 'function'
1667 - ) {
1668 - const wakeable: Wakeable = (thrownValue: any);
1669 - markComponentSuspended(
1670 - erroredWork,
1671 - wakeable,
1672 - workInProgressRootRenderLanes,
1673 - );
1674 - } else {
1675 - markComponentErrored(
1676 - erroredWork,
1677 - thrownValue,
1678 - workInProgressRootRenderLanes,
1679 - );
1680 - }
1681 - }
1720 + if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1721 + // Record the time spent rendering before an error was thrown. This
1722 + // avoids inaccurate Profiler durations in the case of a
1723 + // suspended render.
1724 + stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1725 + }
1726
1683 - const maybeWakeable = throwException(
1684 - root,
1685 - erroredWork.return,
1727 + if (enableSchedulingProfiler) {
1728 + markComponentRenderStopped();
1729 + if (isWakeable) {
1730 + const wakeable: Wakeable = (thrownValue: any);
1731 + markComponentSuspended(
1732 + erroredWork,
1733 + wakeable,
1734 + workInProgressRootRenderLanes,
1735 + );
1736 + } else {
1737 + markComponentErrored(
1738 erroredWork,
1739 thrownValue,
1740 workInProgressRootRenderLanes,
1741 );
1690 - // Setting this to `true` tells the work loop to unwind the stack instead
1691 - // of entering the begin phase. It's called "suspended" because it usually
1692 - // happens because of Suspense, but it also applies to errors. Think of it
1693 - // as suspending the execution of the work loop.
1694 - workInProgressIsSuspended = true;
1695 -
1696 - // Return to the normal work loop.
1697 - return maybeWakeable;
1698 - } catch (yetAnotherThrownValue) {
1699 - // Something in the return path also threw.
1700 - thrownValue = yetAnotherThrownValue;
1701 - if (workInProgress === erroredWork && erroredWork !== null) {
1702 - // If this boundary has already errored, then we had trouble processing
1703 - // the error. Bubble it to the next boundary.
1704 - erroredWork = erroredWork.return;
1705 - workInProgress = erroredWork;
1706 - } else {
1707 - erroredWork = workInProgress;
1708 - }
1709 - continue;
1742 }
1711 - } while (true);
1743 + }
1744 +
1745 + if (isWakeable) {
1746 + const wakeable: Wakeable = (thrownValue: any);
1747 +
1748 + trackSuspendedWakeable(wakeable);
1749 + }
1750 }
1751
1752 function pushDispatcher() {
@@ -1834,7 +1872,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1872 workLoopSync();
1873 break;
1874 } catch (thrownValue) {
1837 - handleError(root, thrownValue);
1875 + handleThrow(root, thrownValue);
1876 }
1877 } while (true);
1878 resetContextDependencies();
@@ -1872,10 +1910,15 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1910 function workLoopSync() {
1911 // Perform work without checking if we need to yield between fiber.
1912
1875 - if (workInProgressIsSuspended && workInProgress !== null) {
1913 + if (workInProgressIsSuspended) {
1914 // The current work-in-progress was already attempted. We need to unwind
1915 // it before we continue the normal work loop.
1878 - resumeSuspendedUnitOfWork(workInProgress);
1916 + const thrownValue = workInProgressThrownValue;
1917 + workInProgressIsSuspended = false;
1918 + workInProgressThrownValue = null;
1919 + if (workInProgress !== null) {
1920 + resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1921 + }
1922 }
1923
1924 while (workInProgress !== null) {
@@ -1927,12 +1970,11 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1970 workLoopConcurrent();
1971 break;
1972 } catch (thrownValue) {
1930 - const maybeWakeable = handleError(root, thrownValue);
1931 - if (maybeWakeable !== null) {
1973 + handleThrow(root, thrownValue);
1974 + if (isTrackingSuspendedThenable()) {
1975 // If this fiber just suspended, it's possible the data is already
1976 // cached. Yield to the the main thread to give it a chance to ping. If
1977 // it does, we can retry immediately without unwinding the stack.
1935 - trackSuspendedWakeable(maybeWakeable);
1978 break;
1979 }
1980 }
@@ -1974,10 +2016,15 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2016 function workLoopConcurrent() {
2017 // Perform work until Scheduler asks us to yield
2018
1977 - if (workInProgressIsSuspended && workInProgress !== null) {
2019 + if (workInProgressIsSuspended) {
2020 // The current work-in-progress was already attempted. We need to unwind
2021 // it before we continue the normal work loop.
1980 - resumeSuspendedUnitOfWork(workInProgress);
2022 + const thrownValue = workInProgressThrownValue;
2023 + workInProgressIsSuspended = false;
2024 + workInProgressThrownValue = null;
2025 + if (workInProgress !== null) {
2026 + resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2027 + }
2028 }
2029
2030 while (workInProgress !== null && !shouldYield()) {
@@ -2013,27 +2060,72 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2060 ReactCurrentOwner.current = null;
2061 }
2062
2016 -function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
2063 +function resumeSuspendedUnitOfWork(
2064 + unitOfWork: Fiber,
2065 + thrownValue: mixed,
2066 +): void {
2067 // This is a fork of performUnitOfWork specifcally for resuming a fiber that
2068 // just suspended. In some cases, we may choose to retry the fiber immediately
2069 // instead of unwinding the stack. It's a separate function to keep the
2070 // additional logic out of the work loop's hot path.
2071
2022 - if (!suspendedWakeableWasPinged()) {
2023 - // The wakeable wasn't pinged. Return to the normal work loop. This will
2072 + const wasPinged = suspendedThenableDidResolve();
2073 + resetWakeableStateAfterEachAttempt();
2074 +
2075 + if (!wasPinged) {
2076 + // The thenable wasn't pinged. Return to the normal work loop. This will
2077 // unwind the stack, and potentially result in showing a fallback.
2025 - workInProgressIsSuspended = false;
2026 - resetWakeableState();
2078 + resetThenableStateOnCompletion();
2079 +
2080 + const returnFiber = unitOfWork.return;
2081 + if (returnFiber === null || workInProgressRoot === null) {
2082 + // Expected to be working on a non-root fiber. This is a fatal error
2083 + // because there's no ancestor that can handle it; the root is
2084 + // supposed to capture all errors that weren't caught by an error
2085 + // boundary.
2086 + workInProgressRootExitStatus = RootFatalErrored;
2087 + workInProgressRootFatalError = thrownValue;
2088 + // Set `workInProgress` to null. This represents advancing to the next
2089 + // sibling, or the parent if there are no siblings. But since the root
2090 + // has no siblings nor a parent, we set it to null. Usually this is
2091 + // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2092 + // intentionally not calling those, we need set it here.
2093 + // TODO: Consider calling `unwindWork` to pop the contexts.
2094 + workInProgress = null;
2095 + return;
2096 + }
2097 +
2098 + try {
2099 + // Find and mark the nearest Suspense or error boundary that can handle
2100 + // this "exception".
2101 + throwException(
2102 + workInProgressRoot,
2103 + returnFiber,
2104 + unitOfWork,
2105 + thrownValue,
2106 + workInProgressRootRenderLanes,
2107 + );
2108 + } catch (error) {
2109 + // We had trouble processing the error. An example of this happening is
2110 + // when accessing the `componentDidCatch` property of an error boundary
2111 + // throws an error. A weird edge case. There's a regression test for this.
2112 + // To prevent an infinite loop, bubble the error up to the next parent.
2113 + workInProgress = returnFiber;
2114 + throw error;
2115 + }
2116 +
2117 + // Return to the normal work loop.
2118 completeUnitOfWork(unitOfWork);
2119 return;
2120 }
2121
2122 // The work-in-progress was immediately pinged. Instead of unwinding the
2032 - // stack and potentially showing a fallback, reset the fiber and try rendering
2033 - // it again.
2123 + // stack and potentially showing a fallback, unwind only the last stack frame,
2124 + // reset the fiber, and try rendering it again.
2125 + const current = unitOfWork.alternate;
2126 + unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2127 unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
2128
2036 - const current = unitOfWork.alternate;
2129 setCurrentDebugFiberInDEV(unitOfWork);
2130
2131 let next;
@@ -2048,8 +2140,7 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
2140 // The begin phase finished successfully without suspending. Reset the state
2141 // used to track the fiber while it was suspended. Then return to the normal
2142 // work loop.
2051 - workInProgressIsSuspended = false;
2052 - resetWakeableState();
2143 + resetThenableStateOnCompletion();
2144
2145 resetCurrentDebugFiberInDEV();
2146 unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2850,7 +2941,53 @@ export function captureCommitPhaseError(
2941 }
2942 }
2943
2853 -export function pingSuspendedRoot(
2944 +export function attachPingListener(
2945 + root: FiberRoot,
2946 + wakeable: Wakeable,
2947 + lanes: Lanes,
2948 +) {
2949 + // Attach a ping listener
2950 + //
2951 + // The data might resolve before we have a chance to commit the fallback. Or,
2952 + // in the case of a refresh, we'll never commit a fallback. So we need to
2953 + // attach a listener now. When it resolves ("pings"), we can decide whether to
2954 + // try rendering the tree again.
2955 + //
2956 + // Only attach a listener if one does not already exist for the lanes
2957 + // we're currently rendering (which acts like a "thread ID" here).
2958 + //
2959 + // We only need to do this in concurrent mode. Legacy Suspense always
2960 + // commits fallbacks synchronously, so there are no pings.
2961 + let pingCache = root.pingCache;
2962 + let threadIDs;
2963 + if (pingCache === null) {
2964 + pingCache = root.pingCache = new PossiblyWeakMap();
2965 + threadIDs = new Set();
2966 + pingCache.set(wakeable, threadIDs);
2967 + } else {
2968 + threadIDs = pingCache.get(wakeable);
2969 + if (threadIDs === undefined) {
2970 + threadIDs = new Set();
2971 + pingCache.set(wakeable, threadIDs);
2972 + }
2973 + }
2974 + if (!threadIDs.has(lanes)) {
2975 + workInProgressRootDidAttachPingListener = true;
2976 +
2977 + // Memoize using the thread ID to prevent redundant listeners.
2978 + threadIDs.add(lanes);
2979 + const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
2980 + if (enableUpdaterTracking) {
2981 + if (isDevToolsPresent) {
2982 + // If we have pending work still, restore the original updaters
2983 + restorePendingUpdaters(root, lanes);
2984 + }
2985 + }
2986 + wakeable.then(ping, ping);
2987 + }
2988 +}
2989 +
2990 +function pingSuspendedRoot(
2991 root: FiberRoot,
2992 wakeable: Wakeable,
2993 pingedLanes: Lanes,
@@ -2874,31 +3011,26 @@ export function pingSuspendedRoot(
3011 // Received a ping at the same priority level at which we're currently
3012 // rendering. We might want to restart this render. This should mirror
3013 // the logic of whether or not a root suspends once it completes.
2877 - const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2878 - if (didPingSuspendedWakeable) {
2879 - // Successfully pinged the in-progress fiber. Don't unwind the stack.
2880 - } else {
2881 - // TODO: If we're rendering sync either due to Sync, Batched or expired,
2882 - // we should probably never restart.
3014 + // TODO: If we're rendering sync either due to Sync, Batched or expired,
3015 + // we should probably never restart.
3016
2884 - // If we're suspended with delay, or if it's a retry, we'll always suspend
2885 - // so we can always restart.
2886 - if (
2887 - workInProgressRootExitStatus === RootSuspendedWithDelay ||
2888 - (workInProgressRootExitStatus === RootSuspended &&
2889 - includesOnlyRetries(workInProgressRootRenderLanes) &&
2890 - now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2891 - ) {
2892 - // Restart from the root.
2893 - prepareFreshStack(root, NoLanes);
2894 - } else {
2895 - // Even though we can't restart right now, we might get an
2896 - // opportunity later. So we mark this render as having a ping.
2897 - workInProgressRootPingedLanes = mergeLanes(
2898 - workInProgressRootPingedLanes,
2899 - pingedLanes,
2900 - );
2901 - }
3017 + // If we're suspended with delay, or if it's a retry, we'll always suspend
3018 + // so we can always restart.
3019 + if (
3020 + workInProgressRootExitStatus === RootSuspendedWithDelay ||
3021 + (workInProgressRootExitStatus === RootSuspended &&
3022 + includesOnlyRetries(workInProgressRootRenderLanes) &&
3023 + now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
3024 + ) {
3025 + // Restart from the root.
3026 + prepareFreshStack(root, NoLanes);
3027 + } else {
3028 + // Even though we can't restart right now, we might get an
3029 + // opportunity later. So we mark this render as having a ping.
3030 + workInProgressRootPingedLanes = mergeLanes(
3031 + workInProgressRootPingedLanes,
3032 + pingedLanes,
3033 + );
3034 }
3035 }
3036
@@ -3172,7 +3304,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
3304 throw originalError;
3305 }
3306
3175 - // Keep this code in sync with handleError; any changes here must have
3307 + // Keep this code in sync with handleThrow; any changes here must have
3308 // corresponding changes there.
3309 resetContextDependencies();
3310 resetHooksAfterThrow();
packages/react-reconciler/src/ReactInternalTypes.js
+3
@@ -17,6 +17,7 @@ import type {
17 MutableSource,
18 StartTransitionOptions,
19 Wakeable,
20 + Usable,
21 } from 'shared/ReactTypes';
22 import type {SuspenseInstance} from './ReactFiberHostConfig';
23 import type {WorkTag} from './ReactWorkTags';
@@ -238,6 +239,7 @@ type BaseFiberRootProperties = {|
239 pingedLanes: Lanes,
240 expiredLanes: Lanes,
241 mutableReadLanes: Lanes,
242 + errorRecoveryDisabledLanes: Lanes,
243
244 finishedLanes: Lanes,
245
@@ -353,6 +355,7 @@ type BasicStateAction<S> = (S => S) | S;
355 type Dispatch<A> = A => void;
356
357 export type Dispatcher = {|
358 + use?: <T>(Usable<T>) => T,
359 getCacheSignal?: () => AbortSignal,
360 getCacheForType?: <T>(resourceType: () => T) => T,
361 readContext<T>(context: ReactContext<T>): T,
packages/react-reconciler/src/__tests__/ReactWakeable-test.js
+260 -9
@@ -4,6 +4,7 @@ let React;
4 let ReactNoop;
5 let Scheduler;
6 let act;
7 +let use;
8 let Suspense;
9 let startTransition;
10
@@ -15,6 +16,7 @@ describe('ReactWakeable', () => {
16 ReactNoop = require('react-noop-renderer');
17 Scheduler = require('scheduler');
18 act = require('jest-react').act;
19 + use = React.experimental_use;
20 Suspense = React.Suspense;
21 startTransition = React.startTransition;
22 });
@@ -45,23 +47,272 @@ describe('ReactWakeable', () => {
47 );
48 }
49
50 + const root = ReactNoop.createRoot();
51 await act(async () => {
52 startTransition(() => {
50 - ReactNoop.render(<App />);
53 + root.render(<App />);
54 });
55 + });
56
57 + expect(Scheduler).toHaveYielded([
58 // React will yield when the async component suspends.
54 - expect(Scheduler).toFlushUntilNextPaint(['Suspend!']);
59 + 'Suspend!',
60 + 'Resolve in microtask',
61 +
62 + // Finished rendering without unwinding the stack or preparing a fallback.
63 + 'Async',
64 + ]);
65 + expect(root).toMatchRenderedOutput('Async');
66 + });
67 +
68 + test('if suspended fiber is pinged in a microtask, it does not block a transition from completing', async () => {
69 + let resolved = false;
70 + function Async() {
71 + if (resolved) {
72 + return <Text text="Async" />;
73 + }
74 + Scheduler.unstable_yieldValue('Suspend!');
75 + throw Promise.resolve().then(() => {
76 + Scheduler.unstable_yieldValue('Resolve in microtask');
77 + resolved = true;
78 + });
79 + }
80 +
81 + function App() {
82 + return <Async />;
83 + }
84 +
85 + const root = ReactNoop.createRoot();
86 + await act(async () => {
87 + startTransition(() => {
88 + root.render(<App />);
89 + });
90 + });
91 + expect(Scheduler).toHaveYielded([
92 + 'Suspend!',
93 + 'Resolve in microtask',
94 + 'Async',
95 + ]);
96 + expect(root).toMatchRenderedOutput('Async');
97 + });
98 +
99 + test('does not infinite loop if already resolved thenable is thrown', async () => {
100 + // An already resolved promise should never be thrown. Since it already
101 + // resolved, we shouldn't bother trying to render again — doing so would
102 + // likely lead to an infinite loop. This scenario should only happen if a
103 + // userspace Suspense library makes an implementation mistake.
104 +
105 + // Create an already resolved thenable
106 + const thenable = {
107 + then(ping) {},
108 + status: 'fulfilled',
109 + value: null,
110 + };
111 +
112 + let i = 0;
113 + function Async() {
114 + if (i++ > 50) {
115 + throw new Error('Infinite loop detected');
116 + }
117 + Scheduler.unstable_yieldValue('Suspend!');
118 + // This thenable should never be thrown because it already resolved.
119 + // But if it is thrown, React should handle it gracefully.
120 + throw thenable;
121 + }
122 +
123 + function App() {
124 + return (
125 + <Suspense fallback={<Text text="Loading..." />}>
126 + <Async />
127 + </Suspense>
128 + );
129 + }
130 +
131 + const root = ReactNoop.createRoot();
132 + await act(async () => {
133 + root.render(<App />);
134 + });
135 + expect(Scheduler).toHaveYielded(['Suspend!', 'Loading...']);
136 + expect(root).toMatchRenderedOutput('Loading...');
137 + });
138 +
139 + // @gate enableUseHook
140 + test('basic use(promise)', async () => {
141 + const promiseA = Promise.resolve('A');
142 + const promiseB = Promise.resolve('B');
143 + const promiseC = Promise.resolve('C');
144 +
145 + function Async() {
146 + const text = use(promiseA) + use(promiseB) + use(promiseC);
147 + return <Text text={text} />;
148 + }
149 +
150 + function App() {
151 + return (
152 + <Suspense fallback={<Text text="Loading..." />}>
153 + <Async />
154 + </Suspense>
155 + );
156 + }
157 +
158 + const root = ReactNoop.createRoot();
159 + await act(async () => {
160 + startTransition(() => {
161 + root.render(<App />);
162 + });
163 + });
164 + expect(Scheduler).toHaveYielded(['ABC']);
165 + expect(root).toMatchRenderedOutput('ABC');
166 + });
167
56 - // Wait for microtasks to resolve
57 - // TODO: The async form of `act` should automatically yield to microtasks
58 - // when a continuation is returned, the way Scheduler does.
59 - await null;
168 + // @gate enableUseHook
169 + test("using a promise that's not cached between attempts", async () => {
170 + function Async() {
171 + const text =
172 + use(Promise.resolve('A')) +
173 + use(Promise.resolve('B')) +
174 + use(Promise.resolve('C'));
175 + return <Text text={text} />;
176 + }
177
61 - expect(Scheduler).toHaveYielded(['Resolve in microtask']);
178 + function App() {
179 + return (
180 + <Suspense fallback={<Text text="Loading..." />}>
181 + <Async />
182 + </Suspense>
183 + );
184 + }
185 +
186 + const root = ReactNoop.createRoot();
187 + await act(async () => {
188 + startTransition(() => {
189 + root.render(<App />);
190 + });
191 });
192 + expect(Scheduler).toHaveYielded(['ABC']);
193 + expect(root).toMatchRenderedOutput('ABC');
194 + });
195 +
196 + // @gate enableUseHook
197 + test('using a rejected promise will throw', async () => {
198 + class ErrorBoundary extends React.Component {
199 + state = {error: null};
200 + static getDerivedStateFromError(error) {
201 + return {error};
202 + }
203 + render() {
204 + if (this.state.error) {
205 + return <Text text={this.state.error.message} />;
206 + }
207 + return this.props.children;
208 + }
209 + }
210 +
211 + const promiseA = Promise.resolve('A');
212 + const promiseB = Promise.reject(new Error('Oops!'));
213 + const promiseC = Promise.resolve('C');
214 +
215 + // Jest/Node will raise an unhandled rejected error unless we await this. It
216 + // works fine in the browser, though.
217 + await expect(promiseB).rejects.toThrow('Oops!');
218 +
219 + function Async() {
220 + const text = use(promiseA) + use(promiseB) + use(promiseC);
221 + return <Text text={text} />;
222 + }
223 +
224 + function App() {
225 + return (
226 + <ErrorBoundary>
227 + <Async />
228 + </ErrorBoundary>
229 + );
230 + }
231 +
232 + const root = ReactNoop.createRoot();
233 + await act(async () => {
234 + startTransition(() => {
235 + root.render(<App />);
236 + });
237 + });
238 + expect(Scheduler).toHaveYielded(['Oops!', 'Oops!']);
239 + });
240 +
241 + // @gate enableUseHook
242 + test('erroring in the same component as an uncached promise does not result in an infinite loop', async () => {
243 + class ErrorBoundary extends React.Component {
244 + state = {error: null};
245 + static getDerivedStateFromError(error) {
246 + return {error};
247 + }
248 + render() {
249 + if (this.state.error) {
250 + return <Text text={'Caught an error: ' + this.state.error.message} />;
251 + }
252 + return this.props.children;
253 + }
254 + }
255 +
256 + let i = 0;
257 + function Async({
258 + // Intentionally destrucutring a prop here so that our production error
259 + // stack trick is triggered at the beginning of the function
260 + prop,
261 + }) {
262 + if (i++ > 50) {
263 + throw new Error('Infinite loop detected');
264 + }
265 + try {
266 + use(Promise.resolve('Async'));
267 + } catch (e) {
268 + Scheduler.unstable_yieldValue('Suspend! [Async]');
269 + throw e;
270 + }
271 + throw new Error('Oops!');
272 + }
273 +
274 + function App() {
275 + return (
276 + <Suspense fallback={<Text text="Loading..." />}>
277 + <ErrorBoundary>
278 + <Async />
279 + </ErrorBoundary>
280 + </Suspense>
281 + );
282 + }
283 +
284 + const root = ReactNoop.createRoot();
285 + await act(async () => {
286 + startTransition(() => {
287 + root.render(<App />);
288 + });
289 + });
290 + expect(Scheduler).toHaveYielded([
291 + // First attempt. The uncached promise suspends.
292 + 'Suspend! [Async]',
293 + // Because the promise already resolved, we're able to unwrap the value
294 + // immediately in a microtask.
295 + //
296 + // Then we proceed to the rest of the component, which throws an error.
297 + 'Caught an error: Oops!',
298 +
299 + // During the sync error recovery pass, the component suspends, because
300 + // we were unable to unwrap the value of the promise.
301 + 'Suspend! [Async]',
302 + 'Loading...',
303 +
304 + // Because the error recovery attempt suspended, React can't tell if the
305 + // error was actually fixed, or it was masked by the suspended data.
306 + // In this case, it wasn't actually fixed, so if we were to commit the
307 + // suspended fallback, it would enter an endless error recovery loop.
308 + //
309 + // Instead, we disable error recovery for these lanes and start
310 + // over again.
311
64 - // Finished rendering without unwinding the stack.
65 - expect(Scheduler).toHaveYielded(['Async']);
312 + // This time, the error is thrown and we commit the result.
313 + 'Suspend! [Async]',
314 + 'Caught an error: Oops!',
315 + ]);
316 + expect(root).toMatchRenderedOutput('Caught an error: Oops!');
317 });
318 });
packages/react/index.classic.fb.js
+1
@@ -27,6 +27,7 @@ export {
27 createMutableSource as unstable_createMutableSource,
28 createRef,
29 createServerContext,
30 + experimental_use,
31 forwardRef,
32 isValidElement,
33 lazy,
packages/react/index.experimental.js
+1
@@ -24,6 +24,7 @@ export {
24 createFactory,
25 createRef,
26 createServerContext,
27 + experimental_use,
28 forwardRef,
29 isValidElement,
30 lazy,
packages/react/index.js
+1
@@ -49,6 +49,7 @@ export {
49 createMutableSource,
50 createRef,
51 createServerContext,
52 + experimental_use,
53 forwardRef,
54 isValidElement,
55 lazy,
packages/react/index.modern.fb.js
+1
@@ -26,6 +26,7 @@ export {
26 createMutableSource as unstable_createMutableSource,
27 createRef,
28 createServerContext,
29 + experimental_use,
30 forwardRef,
31 isValidElement,
32 lazy,
packages/react/src/React.js
+2
@@ -55,6 +55,7 @@ import {
55 useDeferredValue,
56 useId,
57 useCacheRefresh,
58 + use,
59 useMemoCache,
60 } from './ReactHooks';
61 import {
@@ -128,6 +129,7 @@ export {
129 getCacheForType as unstable_getCacheForType,
130 useCacheRefresh as unstable_useCacheRefresh,
131 REACT_CACHE_TYPE as unstable_Cache,
132 + use as experimental_use,
133 useMemoCache as unstable_useMemoCache,
134 // enableScopeAPI
135 REACT_SCOPE_TYPE as unstable_Scope,
packages/react/src/ReactHooks.js
+7
@@ -14,6 +14,7 @@ import type {
14 MutableSourceSubscribeFn,
15 ReactContext,
16 StartTransitionOptions,
17 + Usable,
18 } from 'shared/ReactTypes';
19
20 import ReactCurrentDispatcher from './ReactCurrentDispatcher';
@@ -205,6 +206,12 @@ export function useCacheRefresh(): <T>(?() => T, ?T) => void {
206 return dispatcher.useCacheRefresh();
207 }
208
209 +export function use<T>(usable: Usable<T>): T {
210 + const dispatcher = resolveDispatcher();
211 + // $FlowFixMe This is unstable, thus optional
212 + return dispatcher.use(usable);
213 +}
214 +
215 export function useMemoCache(size: number): Array<any> {
216 const dispatcher = resolveDispatcher();
217 // $FlowFixMe This is unstable, thus optional
packages/scheduler/src/forks/SchedulerMock.js
+6
@@ -517,6 +517,11 @@ function unstable_flushUntilNextPaint(): void {
517 isFlushing = false;
518 }
519 }
520 + return false;
521 +}
522 +
523 +function unstable_hasPendingWork(): boolean {
524 + return scheduledCallback !== null;
525 }
526
527 function unstable_flushExpired() {
@@ -644,6 +649,7 @@ export {
649 unstable_flushExpired,
650 unstable_clearYields,
651 unstable_flushUntilNextPaint,
652 + unstable_hasPendingWork,
653 unstable_flushAll,
654 unstable_yieldValue,
655 unstable_advanceTime,
packages/shared/ReactFeatureFlags.js
+1
@@ -117,6 +117,7 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
117 export const deletedTreeCleanUpLevel = 3;
118
119 export const enableFloat = __EXPERIMENTAL__;
120 +export const enableUseHook = __EXPERIMENTAL__;
121
122 // Enables unstable_useMemoCache hook, intended as a compilation target for
123 // auto-memoization.
packages/shared/ReactTypes.js
+31 -5
@@ -174,13 +174,36 @@ export interface Wakeable {
174 // The subset of a Promise that React APIs rely on. This resolves a value.
175 // This doesn't require a return value neither from the handler nor the
176 // then function.
177 -export interface Thenable<+R> {
178 - then<U>(
179 - onFulfill: (value: R) => void | Thenable<U> | U,
180 - onReject: (error: mixed) => void | Thenable<U> | U,
181 - ): void | Thenable<U>;
177 +interface ThenableImpl<T> {
178 + then(
179 + onFulfill: (value: T) => mixed,
180 + onReject: (error: mixed) => mixed,
181 + ): void | Wakeable;
182 +}
183 +interface UntrackedThenable<T> extends ThenableImpl<T> {
184 + status?: void;
185 +}
186 +
187 +export interface PendingThenable<T> extends ThenableImpl<T> {
188 + status: 'pending';
189 +}
190 +
191 +export interface FulfilledThenable<T> extends ThenableImpl<T> {
192 + status: 'fulfilled';
193 + value: T;
194 }
195
196 +export interface RejectedThenable<T> extends ThenableImpl<T> {
197 + status: 'rejected';
198 + reason: mixed;
199 +}
200 +
201 +export type Thenable<T> =
202 + | UntrackedThenable<T>
203 + | PendingThenable<T>
204 + | FulfilledThenable<T>
205 + | RejectedThenable<T>;
206 +
207 export type OffscreenMode =
208 | 'hidden'
209 | 'unstable-defer-without-hiding'
@@ -189,3 +212,6 @@ export type OffscreenMode =
212 export type StartTransitionOptions = {
213 name?: string,
214 };
215 +
216 +// TODO: Add Context support
217 +export type Usable<T> = Thenable<T>;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -50,6 +50,7 @@ export const warnAboutSpreadingKeyToJSX = false;
50 export const enableSuspenseAvoidThisFallback = false;
51 export const enableSuspenseAvoidThisFallbackFizz = false;
52 export const enableCPUSuspense = true;
53 +export const enableUseHook = false;
54 export const enableUseMemoCacheHook = false;
55 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
56 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
40 export const enableSuspenseAvoidThisFallback = false;
41 export const enableSuspenseAvoidThisFallbackFizz = false;
42 export const enableCPUSuspense = false;
43 +export const enableUseHook = false;
44 export const enableUseMemoCacheHook = false;
45 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
46 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
40 export const enableSuspenseAvoidThisFallback = false;
41 export const enableSuspenseAvoidThisFallbackFizz = false;
42 export const enableCPUSuspense = false;
43 +export const enableUseHook = false;
44 export const enableUseMemoCacheHook = false;
45 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
46 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
49 export const enableSuspenseAvoidThisFallback = false;
50 export const enableSuspenseAvoidThisFallbackFizz = false;
51 export const enableCPUSuspense = false;
52 +export const enableUseHook = false;
53 export const enableUseMemoCacheHook = false;
54 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
55 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
40 export const enableSuspenseAvoidThisFallback = true;
41 export const enableSuspenseAvoidThisFallbackFizz = false;
42 export const enableCPUSuspense = false;
43 +export const enableUseHook = false;
44 export const enableUseMemoCacheHook = false;
45 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
46 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
40 export const enableSuspenseAvoidThisFallback = false;
41 export const enableSuspenseAvoidThisFallbackFizz = false;
42 export const enableCPUSuspense = false;
43 +export const enableUseHook = false;
44 export const enableUseMemoCacheHook = false;
45 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
46 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false;
40 export const enableSuspenseAvoidThisFallback = true;
41 export const enableSuspenseAvoidThisFallbackFizz = false;
42 export const enableCPUSuspense = true;
43 +export const enableUseHook = false;
44 export const enableUseMemoCacheHook = false;
45 export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
46 export const enableClientRenderFallbackOnTextMismatch = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -53,6 +53,7 @@ export const enableSuspenseAvoidThisFallback = true;
53 export const enableSuspenseAvoidThisFallbackFizz = false;
54 export const enableCPUSuspense = true;
55 export const enableFloat = false;
56 +export const enableUseHook = true;
57 export const enableUseMemoCacheHook = true;
58
59 // Logs additional User Timing API marks for use with an experimental profiling tool.
scripts/error-codes/codes.json
+2 -1
@@ -422,5 +422,6 @@
422 "434": "`dangerouslySetInnerHTML` does not make sense on <title>.",
423 "435": "Unexpected Suspense handler tag (%s). This is a bug in React.",
424 "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\".",
425 - "437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead."
425 + "437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
426 + "438": "An unsupported type was passed to use(): %s"
427 }