@samitouri / QOS-React-2 / commits / 685ed561f2

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 +}