Skip to content

Commit 03944bf

Browse files
authored
Update Suspense Priority Warning to Include Component that Triggered Update (#16030)
Improved warning whenever lower priority events (ex. data fetching, page load) happen during a high priority update (ex. hover/click events) to include: 1.) Name of component that triggered the high priority update or 2.) Information that the update was triggered on the root
1 parent 3f2cafe commit 03944bf

File tree

6 files changed

+319
-49
lines changed

6 files changed

+319
-49
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {Fiber} from './ReactFiber';
1313
import type {ExpirationTime} from './ReactFiberExpirationTime';
1414
import type {HookEffectTag} from './ReactHookEffectTags';
1515
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16+
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
1617

1718
import ReactSharedInternals from 'shared/ReactSharedInternals';
1819

@@ -48,6 +49,7 @@ import is from 'shared/objectIs';
4849
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
4950
import {revertPassiveEffectsChange} from 'shared/ReactFeatureFlags';
5051
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
52+
import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
5153

5254
const {ReactCurrentDispatcher} = ReactSharedInternals;
5355

@@ -96,6 +98,8 @@ type Update<S, A> = {
9698
eagerReducer: ((S, A) => S) | null,
9799
eagerState: S | null,
98100
next: Update<S, A> | null,
101+
102+
priority?: ReactPriorityLevel,
99103
};
100104

101105
type UpdateQueue<S, A> = {
@@ -1140,6 +1144,9 @@ function dispatchAction<S, A>(
11401144
eagerState: null,
11411145
next: null,
11421146
};
1147+
if (__DEV__) {
1148+
update.priority = getCurrentPriorityLevel();
1149+
}
11431150
if (renderPhaseUpdates === null) {
11441151
renderPhaseUpdates = new Map();
11451152
}
@@ -1176,6 +1183,10 @@ function dispatchAction<S, A>(
11761183
next: null,
11771184
};
11781185

1186+
if (__DEV__) {
1187+
update.priority = getCurrentPriorityLevel();
1188+
}
1189+
11791190
// Append the update to the end of the list.
11801191
const last = queue.last;
11811192
if (last === null) {

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,8 @@ function prepareFreshStack(root, expirationTime) {
795795

796796
if (__DEV__) {
797797
ReactStrictModeWarnings.discardPendingWarnings();
798-
componentsWithSuspendedDiscreteUpdates = null;
798+
componentsThatSuspendedAtHighPri = null;
799+
componentsThatTriggeredHighPriSuspend = null;
799800
}
800801
}
801802

@@ -982,6 +983,8 @@ function renderRoot(
982983
// Set this to null to indicate there's no in-progress render.
983984
workInProgressRoot = null;
984985

986+
flushSuspensePriorityWarningInDEV();
987+
985988
switch (workInProgressRootExitStatus) {
986989
case RootIncomplete: {
987990
invariant(false, 'Should have a work-in-progress.');
@@ -1491,7 +1494,6 @@ function commitRoot(root) {
14911494
function commitRootImpl(root) {
14921495
flushPassiveEffects();
14931496
flushRenderPhaseStrictModeWarningsInDEV();
1494-
flushSuspensePriorityWarningInDEV();
14951497

14961498
invariant(
14971499
(executionContext & (RenderContext | CommitContext)) === NoContext,
@@ -2520,58 +2522,133 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
25202522

25212523
export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
25222524

2523-
let componentsWithSuspendedDiscreteUpdates = null;
2525+
let componentsThatSuspendedAtHighPri = null;
2526+
let componentsThatTriggeredHighPriSuspend = null;
25242527
export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
25252528
if (__DEV__) {
2529+
const currentPriorityLevel = getCurrentPriorityLevel();
25262530
if (
25272531
(sourceFiber.mode & ConcurrentMode) !== NoEffect &&
2528-
// Check if we're currently rendering a discrete update. Ideally, all we
2529-
// would need to do is check the current priority level. But we currently
2530-
// have no rigorous way to distinguish work that was scheduled at user-
2531-
// blocking priority from work that expired a bit and was "upgraded" to
2532-
// a higher priority. That's because we don't schedule separate callbacks
2533-
// for every level, only the highest priority level per root. The priority
2534-
// of subsequent levels is inferred from the expiration time, but this is
2535-
// an imprecise heuristic.
2536-
//
2537-
// However, we do store the last discrete pending update per root. So we
2538-
// can reliably compare to that one. (If we broaden this warning to include
2539-
// high pri updates that aren't discrete, then this won't be sufficient.)
2540-
//
2541-
// My rationale is that it's better for this warning to have false
2542-
// negatives than false positives.
2543-
rootsWithPendingDiscreteUpdates !== null &&
2544-
workInProgressRoot !== null &&
2545-
renderExpirationTime ===
2546-
rootsWithPendingDiscreteUpdates.get(workInProgressRoot)
2532+
(currentPriorityLevel === UserBlockingPriority ||
2533+
currentPriorityLevel === ImmediatePriority)
25472534
) {
2535+
let workInProgressNode = sourceFiber;
2536+
while (workInProgressNode !== null) {
2537+
// Add the component that triggered the suspense
2538+
const current = workInProgressNode.alternate;
2539+
if (current !== null) {
2540+
// TODO: warn component that triggers the high priority
2541+
// suspend is the HostRoot
2542+
switch (workInProgressNode.tag) {
2543+
case ClassComponent:
2544+
// Loop through the component's update queue and see whether the component
2545+
// has triggered any high priority updates
2546+
const updateQueue = current.updateQueue;
2547+
if (updateQueue !== null) {
2548+
let update = updateQueue.firstUpdate;
2549+
while (update !== null) {
2550+
const priorityLevel = update.priority;
2551+
if (
2552+
priorityLevel === UserBlockingPriority ||
2553+
priorityLevel === ImmediatePriority
2554+
) {
2555+
if (componentsThatTriggeredHighPriSuspend === null) {
2556+
componentsThatTriggeredHighPriSuspend = new Set([
2557+
getComponentName(workInProgressNode.type),
2558+
]);
2559+
} else {
2560+
componentsThatTriggeredHighPriSuspend.add(
2561+
getComponentName(workInProgressNode.type),
2562+
);
2563+
}
2564+
break;
2565+
}
2566+
update = update.next;
2567+
}
2568+
}
2569+
break;
2570+
case FunctionComponent:
2571+
case ForwardRef:
2572+
case SimpleMemoComponent:
2573+
if (
2574+
workInProgressNode.memoizedState !== null &&
2575+
workInProgressNode.memoizedState.baseUpdate !== null
2576+
) {
2577+
let update = workInProgressNode.memoizedState.baseUpdate;
2578+
// Loop through the functional component's memoized state to see whether
2579+
// the component has triggered any high pri updates
2580+
while (update !== null) {
2581+
const priority = update.priority;
2582+
if (
2583+
priority === UserBlockingPriority ||
2584+
priority === ImmediatePriority
2585+
) {
2586+
if (componentsThatTriggeredHighPriSuspend === null) {
2587+
componentsThatTriggeredHighPriSuspend = new Set([
2588+
getComponentName(workInProgressNode.type),
2589+
]);
2590+
} else {
2591+
componentsThatTriggeredHighPriSuspend.add(
2592+
getComponentName(workInProgressNode.type),
2593+
);
2594+
}
2595+
break;
2596+
}
2597+
if (
2598+
update.next === workInProgressNode.memoizedState.baseUpdate
2599+
) {
2600+
break;
2601+
}
2602+
update = update.next;
2603+
}
2604+
}
2605+
break;
2606+
default:
2607+
break;
2608+
}
2609+
}
2610+
workInProgressNode = workInProgressNode.return;
2611+
}
2612+
25482613
// Add the component name to a set.
25492614
const componentName = getComponentName(sourceFiber.type);
2550-
if (componentsWithSuspendedDiscreteUpdates === null) {
2551-
componentsWithSuspendedDiscreteUpdates = new Set([componentName]);
2615+
if (componentsThatSuspendedAtHighPri === null) {
2616+
componentsThatSuspendedAtHighPri = new Set([componentName]);
25522617
} else {
2553-
componentsWithSuspendedDiscreteUpdates.add(componentName);
2618+
componentsThatSuspendedAtHighPri.add(componentName);
25542619
}
25552620
}
25562621
}
25572622
}
25582623

25592624
function flushSuspensePriorityWarningInDEV() {
25602625
if (__DEV__) {
2561-
if (componentsWithSuspendedDiscreteUpdates !== null) {
2626+
if (componentsThatSuspendedAtHighPri !== null) {
25622627
const componentNames = [];
2563-
componentsWithSuspendedDiscreteUpdates.forEach(name => {
2628+
componentsThatSuspendedAtHighPri.forEach(name => {
25642629
componentNames.push(name);
25652630
});
2566-
componentsWithSuspendedDiscreteUpdates = null;
2631+
componentsThatSuspendedAtHighPri = null;
2632+
2633+
const componentsThatTriggeredSuspendNames = [];
2634+
if (componentsThatTriggeredHighPriSuspend !== null) {
2635+
componentsThatTriggeredHighPriSuspend.forEach(name =>
2636+
componentsThatTriggeredSuspendNames.push(name),
2637+
);
2638+
}
2639+
2640+
componentsThatTriggeredHighPriSuspend = null;
25672641

2568-
// TODO: A more helpful version of this message could include the names of
2569-
// the component that were updated, not the ones that suspended. To do
2570-
// that we'd need to track all the components that updated during this
2571-
// render, perhaps using the same mechanism as `markRenderEventTime`.
2642+
const componentThatTriggeredSuspenseError =
2643+
componentsThatTriggeredSuspendNames.length > 0
2644+
? '\n' +
2645+
'The components that triggered the update: ' +
2646+
componentsThatTriggeredSuspendNames.sort().join(', ')
2647+
: '';
25722648
warningWithoutStack(
25732649
false,
25742650
'The following components suspended during a user-blocking update: %s' +
2651+
'%s' +
25752652
'\n\n' +
25762653
'Updates triggered by user interactions (e.g. click events) are ' +
25772654
'considered user-blocking by default. They should not suspend. ' +
@@ -2585,6 +2662,7 @@ function flushSuspensePriorityWarningInDEV() {
25852662
'feedback, and another update to perform the actual change.',
25862663
// TODO: Add link to React docs with more information, once it exists
25872664
componentNames.sort().join(', '),
2665+
componentThatTriggeredSuspenseError,
25882666
);
25892667
}
25902668
}

packages/react-reconciler/src/ReactUpdateQueue.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import type {Fiber} from './ReactFiber';
8888
import type {ExpirationTime} from './ReactFiberExpirationTime';
8989
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
90+
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
9091

9192
import {NoWork} from './ReactFiberExpirationTime';
9293
import {
@@ -106,6 +107,7 @@ import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
106107

107108
import invariant from 'shared/invariant';
108109
import warningWithoutStack from 'shared/warningWithoutStack';
110+
import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
109111

110112
export type Update<State> = {
111113
expirationTime: ExpirationTime,
@@ -117,6 +119,9 @@ export type Update<State> = {
117119

118120
next: Update<State> | null,
119121
nextEffect: Update<State> | null,
122+
123+
//DEV only
124+
priority?: ReactPriorityLevel,
120125
};
121126

122127
export type UpdateQueue<State> = {
@@ -197,7 +202,7 @@ export function createUpdate(
197202
expirationTime: ExpirationTime,
198203
suspenseConfig: null | SuspenseConfig,
199204
): Update<*> {
200-
return {
205+
let update: Update<*> = {
201206
expirationTime,
202207
suspenseConfig,
203208

@@ -208,6 +213,10 @@ export function createUpdate(
208213
next: null,
209214
nextEffect: null,
210215
};
216+
if (__DEV__) {
217+
update.priority = getCurrentPriorityLevel();
218+
}
219+
return update;
211220
}
212221

213222
function appendUpdateToQueue<State>(

packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ describe('ReactSuspense', () => {
327327
});
328328

329329
it('throws if tree suspends and none of the Suspense ancestors have a fallback', () => {
330+
spyOnDev(console, 'error');
330331
ReactTestRenderer.create(
331332
<Suspense>
332333
<AsyncText text="Hi" ms={1000} />
@@ -340,6 +341,13 @@ describe('ReactSuspense', () => {
340341
'AsyncText suspended while rendering, but no fallback UI was specified.',
341342
);
342343
expect(Scheduler).toHaveYielded(['Suspend! [Hi]', 'Suspend! [Hi]']);
344+
if (__DEV__) {
345+
expect(console.error).toHaveBeenCalledTimes(2);
346+
expect(console.error.calls.argsFor(0)[0]).toContain(
347+
'Warning: The following components suspended during a user-blocking update: ',
348+
);
349+
expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
350+
}
343351
});
344352

345353
describe('outside concurrent mode', () => {

0 commit comments

Comments
 (0)