Migrate useDeferredValue and useTransition (#17058)
Migrated useDeferredValue and useTransition from Facebook's www repo into ReactFiberHooks.
Luna Ruan committed
Oct 18, 2019 at 12:48 UTC
685ed561f22ea062281a4c570c7067e6020457c4
9 files changed
+544
-19
packages/react-debug-tools/src/ReactDebugHooks.js
+26
-1
@@ -14,8 +14,9 @@ import type {
14
ReactEventResponderListener,
15
} from 'shared/ReactTypes';
16
import type {Fiber} from 'react-reconciler/src/ReactFiber';
17
-import type {Hook} from 'react-reconciler/src/ReactFiberHooks';
17
+import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
18
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
19
+import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
20
21
import ErrorStackParser from 'error-stack-parser';
22
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -236,6 +237,28 @@ function useResponder(
237
};
238
}
239
240
+function useTransition(
241
+ config: SuspenseConfig | null | void,
242
+): [(() => void) => void, boolean] {
243
+ nextHook();
244
+ hookLog.push({
245
+ primitive: 'Transition',
246
+ stackError: new Error(),
247
+ value: config,
248
+ });
249
+ return [callback => {}, false];
250
+}
251
+
252
+function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
253
+ nextHook();
254
+ hookLog.push({
255
+ primitive: 'DeferredValue',
256
+ stackError: new Error(),
257
+ value,
258
+ });
259
+ return value;
260
+}
261
+
262
const Dispatcher: DispatcherType = {
263
readContext,
264
useCallback,
@@ -249,6 +272,8 @@ const Dispatcher: DispatcherType = {
272
useRef,
273
useState,
274
useResponder,
275
+ useTransition,
276
+ useDeferredValue,
277
};
278
279
// Inspect
packages/react-dom/src/server/ReactPartialRendererHooks.js
+22
-2
@@ -7,13 +7,16 @@
7
* @flow
8
*/
9
10
-import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
10
+import type {
11
+ Dispatcher as DispatcherType,
12
+ TimeoutConfig,
13
+} from 'react-reconciler/src/ReactFiberHooks';
14
import type {ThreadID} from './ReactThreadIDAllocator';
15
import type {
16
ReactContext,
17
ReactEventResponderListener,
18
} from 'shared/ReactTypes';
16
-
19
+import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
20
import {validateContextBounds} from './ReactPartialRendererContext';
21
22
import invariant from 'shared/invariant';
@@ -457,6 +460,21 @@ function useResponder(responder, props): ReactEventResponderListener<any, any> {
460
};
461
}
462
463
+function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
464
+ resolveCurrentlyRenderingComponent();
465
+ return value;
466
+}
467
+
468
+function useTransition(
469
+ config: SuspenseConfig | null | void,
470
+): [(callback: () => void) => void, boolean] {
471
+ resolveCurrentlyRenderingComponent();
472
+ const startTransition = callback => {
473
+ callback();
474
+ };
475
+ return [startTransition, false];
476
+}
477
+
478
function noop(): void {}
479
480
export let currentThreadID: ThreadID = 0;
@@ -481,4 +499,6 @@ export const Dispatcher: DispatcherType = {
499
// Debugging effect
500
useDebugValue: noop,
501
useResponder,
502
+ useDeferredValue,
503
+ useTransition,
504
};
packages/react-reconciler/src/ReactFiberHooks.js
+173
-2
@@ -19,6 +19,7 @@ import type {HookEffectTag} from './ReactHookEffectTags';
19
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
20
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
21
22
+import * as Scheduler from 'scheduler';
23
import ReactSharedInternals from 'shared/ReactSharedInternals';
24
25
import {NoWork} from './ReactFiberExpirationTime';
@@ -54,7 +55,7 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
55
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
56
import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
57
57
-const {ReactCurrentDispatcher} = ReactSharedInternals;
58
+const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
59
60
export type Dispatcher = {
61
readContext<T>(
@@ -92,6 +93,10 @@ export type Dispatcher = {
93
responder: ReactEventResponder<E, C>,
94
props: Object,
95
): ReactEventResponderListener<E, C>,
96
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T,
97
+ useTransition(
98
+ config: SuspenseConfig | void | null,
99
+ ): [(() => void) => void, boolean],
100
};
101
102
type Update<S, A> = {
@@ -123,7 +128,9 @@ export type HookType =
128
| 'useMemo'
129
| 'useImperativeHandle'
130
| 'useDebugValue'
126
- | 'useResponder';
131
+ | 'useResponder'
132
+ | 'useDeferredValue'
133
+ | 'useTransition';
134
135
let didWarnAboutMismatchedHooksForComponent;
136
if (__DEV__) {
@@ -152,6 +159,10 @@ export type FunctionComponentUpdateQueue = {
159
lastEffect: Effect | null,
160
};
161
162
+export type TimeoutConfig = {|
163
+ timeoutMs: number,
164
+|};
165
+
166
type BasicStateAction<S> = (S => S) | S;
167
168
type Dispatch<A> = A => void;
@@ -1117,6 +1128,96 @@ function updateMemo<T>(
1128
return nextValue;
1129
}
1130
1131
+function mountDeferredValue<T>(
1132
+ value: T,
1133
+ config: TimeoutConfig | void | null,
1134
+): T {
1135
+ const [prevValue, setValue] = mountState(value);
1136
+ mountEffect(
1137
+ () => {
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
+ });
1147
+ },
1148
+ [value, config],
1149
+ );
1150
+ return prevValue;
1151
+}
1152
+
1153
+function updateDeferredValue<T>(
1154
+ value: T,
1155
+ config: TimeoutConfig | void | null,
1156
+): T {
1157
+ const [prevValue, setValue] = updateState(value);
1158
+ updateEffect(
1159
+ () => {
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
+ });
1169
+ },
1170
+ [value, config],
1171
+ );
1172
+ return prevValue;
1173
+}
1174
+
1175
+function mountTransition(
1176
+ config: SuspenseConfig | void | null,
1177
+): [(() => void) => void, boolean] {
1178
+ 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];
1196
+}
1197
+
1198
+function updateTransition(
1199
+ config: SuspenseConfig | void | null,
1200
+): [(() => void) => void, boolean] {
1201
+ 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];
1219
+}
1220
+
1221
function dispatchAction<S, A>(
1222
fiber: Fiber,
1223
queue: UpdateQueue<S, A>,
@@ -1272,6 +1373,8 @@ export const ContextOnlyDispatcher: Dispatcher = {
1373
useState: throwInvalidHookError,
1374
useDebugValue: throwInvalidHookError,
1375
useResponder: throwInvalidHookError,
1376
+ useDeferredValue: throwInvalidHookError,
1377
+ useTransition: throwInvalidHookError,
1378
};
1379
1380
const HooksDispatcherOnMount: Dispatcher = {
@@ -1288,6 +1391,8 @@ const HooksDispatcherOnMount: Dispatcher = {
1391
useState: mountState,
1392
useDebugValue: mountDebugValue,
1393
useResponder: createResponderListener,
1394
+ useDeferredValue: mountDeferredValue,
1395
+ useTransition: mountTransition,
1396
};
1397
1398
const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1304,6 +1409,8 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1409
useState: updateState,
1410
useDebugValue: updateDebugValue,
1411
useResponder: createResponderListener,
1412
+ useDeferredValue: updateDeferredValue,
1413
+ useTransition: updateTransition,
1414
};
1415
1416
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1441,6 +1548,18 @@ if (__DEV__) {
1548
mountHookTypesDev();
1549
return createResponderListener(responder, props);
1550
},
1551
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1552
+ currentHookNameInDev = 'useDeferredValue';
1553
+ mountHookTypesDev();
1554
+ return mountDeferredValue(value, config);
1555
+ },
1556
+ useTransition(
1557
+ config: SuspenseConfig | void | null,
1558
+ ): [(() => void) => void, boolean] {
1559
+ currentHookNameInDev = 'useTransition';
1560
+ mountHookTypesDev();
1561
+ return mountTransition(config);
1562
+ },
1563
};
1564
1565
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -1546,6 +1665,18 @@ if (__DEV__) {
1665
updateHookTypesDev();
1666
return createResponderListener(responder, props);
1667
},
1668
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1669
+ currentHookNameInDev = 'useDeferredValue';
1670
+ updateHookTypesDev();
1671
+ return mountDeferredValue(value, config);
1672
+ },
1673
+ useTransition(
1674
+ config: SuspenseConfig | void | null,
1675
+ ): [(() => void) => void, boolean] {
1676
+ currentHookNameInDev = 'useTransition';
1677
+ updateHookTypesDev();
1678
+ return mountTransition(config);
1679
+ },
1680
};
1681
1682
HooksDispatcherOnUpdateInDEV = {
@@ -1651,6 +1782,18 @@ if (__DEV__) {
1782
updateHookTypesDev();
1783
return createResponderListener(responder, props);
1784
},
1785
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1786
+ currentHookNameInDev = 'useDeferredValue';
1787
+ updateHookTypesDev();
1788
+ return updateDeferredValue(value, config);
1789
+ },
1790
+ useTransition(
1791
+ config: SuspenseConfig | void | null,
1792
+ ): [(() => void) => void, boolean] {
1793
+ currentHookNameInDev = 'useTransition';
1794
+ updateHookTypesDev();
1795
+ return updateTransition(config);
1796
+ },
1797
};
1798
1799
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -1768,6 +1911,20 @@ if (__DEV__) {
1911
mountHookTypesDev();
1912
return createResponderListener(responder, props);
1913
},
1914
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1915
+ currentHookNameInDev = 'useDeferredValue';
1916
+ warnInvalidHookAccess();
1917
+ mountHookTypesDev();
1918
+ return mountDeferredValue(value, config);
1919
+ },
1920
+ useTransition(
1921
+ config: SuspenseConfig | void | null,
1922
+ ): [(() => void) => void, boolean] {
1923
+ currentHookNameInDev = 'useTransition';
1924
+ warnInvalidHookAccess();
1925
+ mountHookTypesDev();
1926
+ return mountTransition(config);
1927
+ },
1928
};
1929
1930
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -1885,5 +2042,19 @@ if (__DEV__) {
2042
updateHookTypesDev();
2043
return createResponderListener(responder, props);
2044
},
2045
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
2046
+ currentHookNameInDev = 'useDeferredValue';
2047
+ warnInvalidHookAccess();
2048
+ updateHookTypesDev();
2049
+ return updateDeferredValue(value, config);
2050
+ },
2051
+ useTransition(
2052
+ config: SuspenseConfig | void | null,
2053
+ ): [(() => void) => void, boolean] {
2054
+ currentHookNameInDev = 'useTransition';
2055
+ warnInvalidHookAccess();
2056
+ updateHookTypesDev();
2057
+ return updateTransition(config);
2058
+ },
2059
};
2060
}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+1
-1
@@ -2930,7 +2930,7 @@ function flushSuspensePriorityWarningInDEV() {
2930
'update to provide immediate feedback, and another update that ' +
2931
'triggers the bulk of the changes.' +
2932
'\n\n' +
2933
- 'Refer to the documentation for useSuspenseTransition to learn how ' +
2933
+ 'Refer to the documentation for useTransition to learn how ' +
2934
'to implement this pattern.',
2935
// TODO: Add link to React docs with more information, once it exists
2936
componentNames.sort().join(', '),
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+25
-13
@@ -1485,6 +1485,31 @@ describe('ReactHooks', () => {
1485
useStateHelper,
1486
];
1487
1488
+ // We don't include useContext or useDebugValue in this set,
1489
+ // because they aren't added to the hooks list and so won't throw.
1490
+ let hooksInList = [
1491
+ useCallbackHelper,
1492
+ useEffectHelper,
1493
+ useImperativeHandleHelper,
1494
+ useLayoutEffectHelper,
1495
+ useMemoHelper,
1496
+ useReducerHelper,
1497
+ useRefHelper,
1498
+ useStateHelper,
1499
+ ];
1500
+
1501
+ if (__EXPERIMENTAL__) {
1502
+ const useTransitionHelper = () => React.useTransition({timeoutMs: 1000});
1503
+ const useDeferredValueHelper = () =>
1504
+ React.useDeferredValue(0, {timeoutMs: 1000});
1505
+
1506
+ orderedHooks.push(useTransitionHelper);
1507
+ orderedHooks.push(useDeferredValueHelper);
1508
+
1509
+ hooksInList.push(useTransitionHelper);
1510
+ hooksInList.push(useDeferredValueHelper);
1511
+ }
1512
+
1513
const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => {
1514
return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${
1515
hookNameB ? `use${hookNameB}` : undefined
@@ -1598,19 +1623,6 @@ describe('ReactHooks', () => {
1623
});
1624
});
1625
1601
- // We don't include useContext or useDebugValue in this set,
1602
- // because they aren't added to the hooks list and so won't throw.
1603
- let hooksInList = [
1604
- useCallbackHelper,
1605
- useEffectHelper,
1606
- useImperativeHandleHelper,
1607
- useLayoutEffectHelper,
1608
- useMemoHelper,
1609
- useReducerHelper,
1610
- useRefHelper,
1611
- useStateHelper,
1612
- ];
1613
-
1626
hooksInList.forEach((firstHelper, index) => {
1627
const secondHelper =
1628
index > 0
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+258
@@ -13,10 +13,13 @@
13
'use strict';
14
15
let React;
16
+let ReactCache;
17
+let TextResource;
18
let ReactFeatureFlags;
19
let ReactNoop;
20
let Scheduler;
21
let SchedulerTracing;
22
+let Suspense;
23
let useState;
24
let useReducer;
25
let useEffect;
@@ -25,6 +28,8 @@ let useCallback;
28
let useMemo;
29
let useRef;
30
let useImperativeHandle;
31
+let useTransition;
32
+let useDeferredValue;
33
let forwardRef;
34
let memo;
35
let act;
@@ -32,14 +37,17 @@ let act;
37
describe('ReactHooksWithNoopRenderer', () => {
38
beforeEach(() => {
39
jest.resetModules();
40
+ jest.useFakeTimers();
41
42
ReactFeatureFlags = require('shared/ReactFeatureFlags');
43
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
44
ReactFeatureFlags.enableSchedulerTracing = true;
45
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
46
React = require('react');
47
ReactNoop = require('react-noop-renderer');
48
Scheduler = require('scheduler');
49
SchedulerTracing = require('scheduler/tracing');
50
+ ReactCache = require('react-cache');
51
useState = React.useState;
52
useReducer = React.useReducer;
53
useEffect = React.useEffect;
@@ -50,18 +58,62 @@ describe('ReactHooksWithNoopRenderer', () => {
58
useImperativeHandle = React.useImperativeHandle;
59
forwardRef = React.forwardRef;
60
memo = React.memo;
61
+ useTransition = React.useTransition;
62
+ useDeferredValue = React.useDeferredValue;
63
+ Suspense = React.Suspense;
64
act = ReactNoop.act;
65
+
66
+ TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
67
+ return new Promise((resolve, reject) =>
68
+ setTimeout(() => {
69
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
70
+ resolve(text);
71
+ }, ms),
72
+ );
73
+ }, ([text, ms]) => text);
74
});
75
76
function span(prop) {
77
return {type: 'span', hidden: false, children: [], prop};
78
}
79
80
+ function hiddenSpan(prop) {
81
+ return {type: 'span', children: [], prop, hidden: true};
82
+ }
83
+
84
function Text(props) {
85
Scheduler.unstable_yieldValue(props.text);
86
return <span prop={props.text} />;
87
}
88
89
+ function AsyncText(props) {
90
+ const text = props.text;
91
+ try {
92
+ TextResource.read([props.text, props.ms]);
93
+ Scheduler.unstable_yieldValue(text);
94
+ return <span prop={text} />;
95
+ } catch (promise) {
96
+ if (typeof promise.then === 'function') {
97
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
98
+ } else {
99
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
100
+ }
101
+ throw promise;
102
+ }
103
+ }
104
+
105
+ function advanceTimers(ms) {
106
+ // Note: This advances Jest's virtual time but not React's. Use
107
+ // ReactNoop.expire for that.
108
+ if (typeof ms !== 'number') {
109
+ throw new Error('Must specify ms');
110
+ }
111
+ jest.advanceTimersByTime(ms);
112
+ // Wait until the end of the current tick
113
+ // We cannot use a timer since we're faking them
114
+ return Promise.resolve().then(() => {});
115
+ }
116
+
117
it('resumes after an interruption', () => {
118
function Counter(props, ref) {
119
const [count, updateCount] = useState(0);
@@ -1921,7 +1973,213 @@ describe('ReactHooksWithNoopRenderer', () => {
1973
expect(totalRefUpdates).toBe(2); // Should not increase since last time
1974
});
1975
});
1976
+ if (__EXPERIMENTAL__) {
1977
+ describe('useTransition', () => {
1978
+ it('delays showing loading state until after timeout', async () => {
1979
+ let transition;
1980
+ function App() {
1981
+ const [show, setShow] = useState(false);
1982
+ const [startTransition, isPending] = useTransition({
1983
+ timeoutMs: 1000,
1984
+ });
1985
+ transition = () => {
1986
+ startTransition(() => {
1987
+ setShow(true);
1988
+ });
1989
+ };
1990
+ return (
1991
+ <Suspense
1992
+ fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
1993
+ {show ? (
1994
+ <AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
1995
+ ) : (
1996
+ <Text text={`Before... Pending: ${isPending}`} />
1997
+ )}
1998
+ </Suspense>
1999
+ );
2000
+ }
2001
+ ReactNoop.render(<App />);
2002
+ expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2003
+ expect(ReactNoop.getChildren()).toEqual([
2004
+ span('Before... Pending: false'),
2005
+ ]);
2006
+
2007
+ act(() => {
2008
+ Scheduler.unstable_runWithPriority(
2009
+ Scheduler.unstable_UserBlockingPriority,
2010
+ transition,
2011
+ );
2012
+ });
2013
+ Scheduler.unstable_advanceTime(500);
2014
+ await advanceTimers(500);
2015
+ expect(Scheduler).toHaveYielded([
2016
+ 'Before... Pending: true',
2017
+ 'Suspend! [After... Pending: false]',
2018
+ 'Loading... Pending: false',
2019
+ ]);
2020
+ expect(ReactNoop.getChildren()).toEqual([
2021
+ span('Before... Pending: true'),
2022
+ ]);
2023
+
2024
+ Scheduler.unstable_advanceTime(1000);
2025
+ await advanceTimers(1000);
2026
+ expect(ReactNoop.getChildren()).toEqual([
2027
+ hiddenSpan('Before... Pending: true'),
2028
+ span('Loading... Pending: false'),
2029
+ ]);
2030
+
2031
+ Scheduler.unstable_advanceTime(500);
2032
+ await advanceTimers(500);
2033
+ expect(Scheduler).toHaveYielded([
2034
+ 'Promise resolved [After... Pending: false]',
2035
+ ]);
2036
+ expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2037
+ expect(ReactNoop.getChildren()).toEqual([
2038
+ span('After... Pending: false'),
2039
+ ]);
2040
+ });
2041
+ it('delays showing loading state until after busyDelayMs + busyMinDurationMs', async () => {
2042
+ let transition;
2043
+ function App() {
2044
+ const [show, setShow] = useState(false);
2045
+ const [startTransition, isPending] = useTransition({
2046
+ busyDelayMs: 1000,
2047
+ busyMinDurationMs: 2000,
2048
+ });
2049
+ transition = () => {
2050
+ startTransition(() => {
2051
+ setShow(true);
2052
+ });
2053
+ };
2054
+ return (
2055
+ <Suspense
2056
+ fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2057
+ {show ? (
2058
+ <AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
2059
+ ) : (
2060
+ <Text text={`Before... Pending: ${isPending}`} />
2061
+ )}
2062
+ </Suspense>
2063
+ );
2064
+ }
2065
+ ReactNoop.render(<App />);
2066
+ expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2067
+ expect(ReactNoop.getChildren()).toEqual([
2068
+ span('Before... Pending: false'),
2069
+ ]);
2070
+
2071
+ act(() => {
2072
+ Scheduler.unstable_runWithPriority(
2073
+ Scheduler.unstable_UserBlockingPriority,
2074
+ transition,
2075
+ );
2076
+ });
2077
+ Scheduler.unstable_advanceTime(1000);
2078
+ await advanceTimers(1000);
2079
+ expect(Scheduler).toHaveYielded([
2080
+ 'Before... Pending: true',
2081
+ 'Suspend! [After... Pending: false]',
2082
+ 'Loading... Pending: false',
2083
+ ]);
2084
+ expect(ReactNoop.getChildren()).toEqual([
2085
+ span('Before... Pending: true'),
2086
+ ]);
2087
+
2088
+ Scheduler.unstable_advanceTime(1000);
2089
+ await advanceTimers(1000);
2090
+ expect(Scheduler).toHaveYielded([
2091
+ 'Promise resolved [After... Pending: false]',
2092
+ ]);
2093
+ expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2094
+ expect(ReactNoop.getChildren()).toEqual([
2095
+ span('Before... Pending: true'),
2096
+ ]);
2097
+
2098
+ Scheduler.unstable_advanceTime(1000);
2099
+ await advanceTimers(1000);
2100
+ expect(ReactNoop.getChildren()).toEqual([
2101
+ span('Before... Pending: true'),
2102
+ ]);
2103
+ Scheduler.unstable_advanceTime(250);
2104
+ await advanceTimers(250);
2105
+ expect(ReactNoop.getChildren()).toEqual([
2106
+ span('After... Pending: false'),
2107
+ ]);
2108
+ });
2109
+ });
2110
+ describe('useDeferredValue', () => {
2111
+ it('defers text value until specified timeout', async () => {
2112
+ function TextBox({text}) {
2113
+ return <AsyncText ms={1000} text={text} />;
2114
+ }
2115
+
2116
+ let _setText;
2117
+ function App() {
2118
+ const [text, setText] = useState('A');
2119
+ const deferredText = useDeferredValue(text, {
2120
+ timeoutMs: 500,
2121
+ });
2122
+ _setText = setText;
2123
+ return (
2124
+ <>
2125
+ <Text text={text} />
2126
+ <Suspense fallback={<Text text={'Loading'} />}>
2127
+ <TextBox text={deferredText} />
2128
+ </Suspense>
2129
+ </>
2130
+ );
2131
+ }
2132
+
2133
+ act(() => {
2134
+ ReactNoop.render(<App />);
2135
+ });
2136
+
2137
+ expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
2138
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]);
2139
+
2140
+ Scheduler.unstable_advanceTime(1000);
2141
+ await advanceTimers(1000);
2142
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2143
+ expect(Scheduler).toFlushAndYield(['A']);
2144
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
2145
+
2146
+ act(() => {
2147
+ _setText('B');
2148
+ });
2149
+ expect(Scheduler).toHaveYielded([
2150
+ 'B',
2151
+ 'A',
2152
+ 'B',
2153
+ 'Suspend! [B]',
2154
+ 'Loading',
2155
+ ]);
2156
+ expect(Scheduler).toFlushAndYield([]);
2157
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2158
+
2159
+ Scheduler.unstable_advanceTime(250);
2160
+ await advanceTimers(250);
2161
+ expect(Scheduler).toFlushAndYield([]);
2162
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2163
+
2164
+ Scheduler.unstable_advanceTime(500);
2165
+ await advanceTimers(500);
2166
+ expect(ReactNoop.getChildren()).toEqual([
2167
+ span('B'),
2168
+ hiddenSpan('A'),
2169
+ span('Loading'),
2170
+ ]);
2171
+
2172
+ Scheduler.unstable_advanceTime(250);
2173
+ await advanceTimers(250);
2174
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2175
2176
+ act(() => {
2177
+ expect(Scheduler).toFlushAndYield(['B']);
2178
+ });
2179
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
2180
+ });
2181
+ });
2182
+ }
2183
describe('progressive enhancement (not supported)', () => {
2184
it('mount additional state', () => {
2185
let updateA;
packages/react-test-renderer/src/ReactShallowRenderer.js
+19
@@ -377,6 +377,23 @@ class ReactShallowRenderer {
377
responder,
378
});
379
380
+ // TODO: implement if we decide to keep the shallow renderer
381
+ const useTransition = (
382
+ config,
383
+ ): [(callback: () => void) => void, boolean] => {
384
+ this._validateCurrentlyRenderingComponent();
385
+ const startTransition = callback => {
386
+ callback();
387
+ };
388
+ return [startTransition, false];
389
+ };
390
+
391
+ // TODO: implement if we decide to keep the shallow renderer
392
+ const useDeferredValue = <T>(value: T, config): T => {
393
+ this._validateCurrentlyRenderingComponent();
394
+ return value;
395
+ };
396
+
397
return {
398
readContext,
399
useCallback: (identity: any),
@@ -393,6 +410,8 @@ class ReactShallowRenderer {
410
useRef,
411
useState,
412
useResponder,
413
+ useTransition,
414
+ useDeferredValue,
415
};
416
}
417
packages/react/src/React.js
+8
@@ -40,6 +40,8 @@ import {
40
useRef,
41
useState,
42
useResponder,
43
+ useTransition,
44
+ useDeferredValue,
45
} from './ReactHooks';
46
import {withSuspenseConfig} from './ReactBatchConfig';
47
import {
@@ -59,6 +61,7 @@ import {
61
enableFlareAPI,
62
enableFundamentalAPI,
63
enableScopeAPI,
64
+ exposeConcurrentModeAPIs,
65
} from 'shared/ReactFeatureFlags';
66
const React = {
67
Children: {
@@ -107,6 +110,11 @@ const React = {
110
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
111
};
112
113
+if (exposeConcurrentModeAPIs) {
114
+ React.useTransition = useTransition;
115
+ React.useDeferredValue = useDeferredValue;
116
+}
117
+
118
if (enableFlareAPI) {
119
React.unstable_useResponder = useResponder;
120
React.unstable_createResponder = createResponder;
packages/react/src/ReactHooks.js
+12
@@ -160,3 +160,15 @@ export function useResponder(
160
}
161
return dispatcher.useResponder(responder, listenerProps || emptyObject);
162
}
163
+
164
+export function useTransition(
165
+ config: ?Object,
166
+): [(() => void) => void, boolean] {
167
+ const dispatcher = resolveDispatcher();
168
+ return dispatcher.useTransition(config);
169
+}
170
+
171
+export function useDeferredValue<T>(value: T, config: ?Object): T {
172
+ const dispatcher = resolveDispatcher();
173
+ return dispatcher.useDeferredValue(value, config);
174
+}