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,
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
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) {
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
}
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
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.
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
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;
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() {
1872
workLoopSync();
1873
break;
1874
} catch (thrownValue) {
1837
- handleError(root, thrownValue);
1875
+ handleThrow(root, thrownValue);
1876
}
1877
} while (true);
1878
resetContextDependencies();
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) {
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
}
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()) {
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;
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;
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,
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
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();