[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
+});