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

[Bugfix] Pending state is always user-blocking (#17382)

Fixes a bug where `isPending` is only set to `true` if `startTransition` is called from inside an input event. That's usually the case, but not always. Now it works regardless of where you call it.

Andrew Clark committed Nov 15, 2019 at 15:46 UTC 2586303662736d72e79046b51c8df993b21e6093
2 files changed +155 -54
packages/react-reconciler/src/ReactFiberHooks.js
+53 -54
@@ -19,7 +19,6 @@ import type {HookEffectTag} from './ReactHookEffectTags';
19 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
20 import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
21
22 -import * as Scheduler from 'scheduler';
22 import ReactSharedInternals from 'shared/ReactSharedInternals';
23
24 import {NoWork} from './ReactFiberExpirationTime';
@@ -53,7 +52,12 @@ import getComponentName from 'shared/getComponentName';
52 import is from 'shared/objectIs';
53 import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
54 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
56 -import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
55 +import {
56 + UserBlockingPriority,
57 + NormalPriority,
58 + runWithPriority,
59 + getCurrentPriorityLevel,
60 +} from './SchedulerWithReactIntegration';
61
62 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
63
@@ -1135,15 +1139,13 @@ function mountDeferredValue<T>(
1139 const [prevValue, setValue] = mountState(value);
1140 mountEffect(
1141 () => {
1138 - Scheduler.unstable_next(() => {
1139 - const previousConfig = ReactCurrentBatchConfig.suspense;
1140 - ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1141 - try {
1142 - setValue(value);
1143 - } finally {
1144 - ReactCurrentBatchConfig.suspense = previousConfig;
1145 - }
1146 - });
1142 + const previousConfig = ReactCurrentBatchConfig.suspense;
1143 + ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1144 + try {
1145 + setValue(value);
1146 + } finally {
1147 + ReactCurrentBatchConfig.suspense = previousConfig;
1148 + }
1149 },
1150 [value, config],
1151 );
@@ -1157,65 +1159,62 @@ function updateDeferredValue<T>(
1159 const [prevValue, setValue] = updateState(value);
1160 updateEffect(
1161 () => {
1160 - Scheduler.unstable_next(() => {
1161 - const previousConfig = ReactCurrentBatchConfig.suspense;
1162 - ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1163 - try {
1164 - setValue(value);
1165 - } finally {
1166 - ReactCurrentBatchConfig.suspense = previousConfig;
1167 - }
1168 - });
1162 + const previousConfig = ReactCurrentBatchConfig.suspense;
1163 + ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1164 + try {
1165 + setValue(value);
1166 + } finally {
1167 + ReactCurrentBatchConfig.suspense = previousConfig;
1168 + }
1169 },
1170 [value, config],
1171 );
1172 return prevValue;
1173 }
1174
1175 +function startTransition(setPending, config, callback) {
1176 + const priorityLevel = getCurrentPriorityLevel();
1177 + runWithPriority(
1178 + priorityLevel < UserBlockingPriority ? UserBlockingPriority : priorityLevel,
1179 + () => {
1180 + setPending(true);
1181 + },
1182 + );
1183 + runWithPriority(
1184 + priorityLevel > NormalPriority ? NormalPriority : priorityLevel,
1185 + () => {
1186 + const previousConfig = ReactCurrentBatchConfig.suspense;
1187 + ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1188 + try {
1189 + setPending(false);
1190 + callback();
1191 + } finally {
1192 + ReactCurrentBatchConfig.suspense = previousConfig;
1193 + }
1194 + },
1195 + );
1196 +}
1197 +
1198 function mountTransition(
1199 config: SuspenseConfig | void | null,
1200 ): [(() => void) => void, boolean] {
1201 const [isPending, setPending] = mountState(false);
1179 - const startTransition = mountCallback(
1180 - callback => {
1181 - setPending(true);
1182 - Scheduler.unstable_next(() => {
1183 - const previousConfig = ReactCurrentBatchConfig.suspense;
1184 - ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1185 - try {
1186 - setPending(false);
1187 - callback();
1188 - } finally {
1189 - ReactCurrentBatchConfig.suspense = previousConfig;
1190 - }
1191 - });
1192 - },
1193 - [config, isPending],
1194 - );
1195 - return [startTransition, isPending];
1202 + const start = mountCallback(startTransition.bind(null, setPending, config), [
1203 + setPending,
1204 + config,
1205 + ]);
1206 + return [start, isPending];
1207 }
1208
1209 function updateTransition(
1210 config: SuspenseConfig | void | null,
1211 ): [(() => void) => void, boolean] {
1212 const [isPending, setPending] = updateState(false);
1202 - const startTransition = updateCallback(
1203 - callback => {
1204 - setPending(true);
1205 - Scheduler.unstable_next(() => {
1206 - const previousConfig = ReactCurrentBatchConfig.suspense;
1207 - ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1208 - try {
1209 - setPending(false);
1210 - callback();
1211 - } finally {
1212 - ReactCurrentBatchConfig.suspense = previousConfig;
1213 - }
1214 - });
1215 - },
1216 - [config, isPending],
1217 - );
1218 - return [startTransition, isPending];
1213 + const start = updateCallback(startTransition.bind(null, setPending, config), [
1214 + setPending,
1215 + config,
1216 + ]);
1217 + return [start, isPending];
1218 }
1219
1220 function dispatchAction<S, A>(
packages/react-reconciler/src/__tests__/ReactTransition-test.internal.js new
+102
@@ -0,0 +1,102 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +'use strict';
12 +
13 +let ReactFeatureFlags;
14 +let React;
15 +let ReactNoop;
16 +let Scheduler;
17 +let Suspense;
18 +let useState;
19 +let useTransition;
20 +let act;
21 +
22 +describe('ReactHooksWithNoopRenderer', () => {
23 + beforeEach(() => {
24 + jest.resetModules();
25 +
26 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
27 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
28 + ReactFeatureFlags.enableSchedulerTracing = true;
29 + ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
30 + React = require('react');
31 + ReactNoop = require('react-noop-renderer');
32 + Scheduler = require('scheduler');
33 + useState = React.useState;
34 + useTransition = React.useTransition;
35 + Suspense = React.Suspense;
36 + act = ReactNoop.act;
37 + });
38 +
39 + function Text(props) {
40 + Scheduler.unstable_yieldValue(props.text);
41 + return props.text;
42 + }
43 +
44 + function createAsyncText(text) {
45 + let resolved = false;
46 + let Component = function() {
47 + if (!resolved) {
48 + Scheduler.unstable_yieldValue('Suspend! [' + text + ']');
49 + throw promise;
50 + }
51 + return <Text text={text} />;
52 + };
53 + let promise = new Promise(resolve => {
54 + Component.resolve = function() {
55 + resolved = true;
56 + return resolve();
57 + };
58 + });
59 + return Component;
60 + }
61 +
62 + it('isPending works even if called from outside an input event', async () => {
63 + const Async = createAsyncText('Async');
64 + let start;
65 + function App() {
66 + const [show, setShow] = useState(false);
67 + const [startTransition, isPending] = useTransition();
68 + start = () => startTransition(() => setShow(true));
69 + return (
70 + <Suspense fallback={<Text text="Loading..." />}>
71 + {isPending ? <Text text="Pending..." /> : null}
72 + {show ? <Async /> : <Text text="(empty)" />}
73 + </Suspense>
74 + );
75 + }
76 +
77 + const root = ReactNoop.createRoot();
78 +
79 + await act(async () => {
80 + root.render(<App />);
81 + });
82 + expect(Scheduler).toHaveYielded(['(empty)']);
83 + expect(root).toMatchRenderedOutput('(empty)');
84 +
85 + await act(async () => {
86 + start();
87 +
88 + expect(Scheduler).toFlushAndYield([
89 + 'Pending...',
90 + '(empty)',
91 + 'Suspend! [Async]',
92 + 'Loading...',
93 + ]);
94 +
95 + expect(root).toMatchRenderedOutput('Pending...(empty)');
96 +
97 + await Async.resolve();
98 + });
99 + expect(Scheduler).toHaveYielded(['Async']);
100 + expect(root).toMatchRenderedOutput('Async');
101 + });
102 +});