diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 994745b8a75ae..07e94cd720ed1 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -14,8 +14,9 @@ import type { ReactEventResponderListener, } from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {Hook} from 'react-reconciler/src/ReactFiberHooks'; +import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks'; import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks'; +import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig'; import ErrorStackParser from 'error-stack-parser'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -236,6 +237,28 @@ function useResponder( }; } +function useTransition( + config: SuspenseConfig | null | void, +): [(() => void) => void, boolean] { + nextHook(); + hookLog.push({ + primitive: 'Transition', + stackError: new Error(), + value: config, + }); + return [callback => {}, false]; +} + +function useDeferredValue(value: T, config: TimeoutConfig | null | void): T { + nextHook(); + hookLog.push({ + primitive: 'DeferredValue', + stackError: new Error(), + value, + }); + return value; +} + const Dispatcher: DispatcherType = { readContext, useCallback, @@ -249,6 +272,8 @@ const Dispatcher: DispatcherType = { useRef, useState, useResponder, + useTransition, + useDeferredValue, }; // Inspect diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index dbe2c4f7816c4..d054caf53012f 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -7,13 +7,16 @@ * @flow */ -import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks'; +import type { + Dispatcher as DispatcherType, + TimeoutConfig, +} from 'react-reconciler/src/ReactFiberHooks'; import type {ThreadID} from './ReactThreadIDAllocator'; import type { ReactContext, ReactEventResponderListener, } from 'shared/ReactTypes'; - +import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig'; import {validateContextBounds} from './ReactPartialRendererContext'; import invariant from 'shared/invariant'; @@ -457,6 +460,21 @@ function useResponder(responder, props): ReactEventResponderListener { }; } +function useDeferredValue(value: T, config: TimeoutConfig | null | void): T { + resolveCurrentlyRenderingComponent(); + return value; +} + +function useTransition( + config: SuspenseConfig | null | void, +): [(callback: () => void) => void, boolean] { + resolveCurrentlyRenderingComponent(); + const startTransition = callback => { + callback(); + }; + return [startTransition, false]; +} + function noop(): void {} export let currentThreadID: ThreadID = 0; @@ -481,4 +499,6 @@ export const Dispatcher: DispatcherType = { // Debugging effect useDebugValue: noop, useResponder, + useDeferredValue, + useTransition, }; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 3f0552ba30210..707a65ebef2ce 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -19,6 +19,7 @@ import type {HookEffectTag} from './ReactHookEffectTags'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; +import * as Scheduler from 'scheduler'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {NoWork} from './ReactFiberExpirationTime'; @@ -54,7 +55,7 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration'; -const {ReactCurrentDispatcher} = ReactSharedInternals; +const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; export type Dispatcher = { readContext( @@ -92,6 +93,10 @@ export type Dispatcher = { responder: ReactEventResponder, props: Object, ): ReactEventResponderListener, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean], }; type Update = { @@ -123,7 +128,9 @@ export type HookType = | 'useMemo' | 'useImperativeHandle' | 'useDebugValue' - | 'useResponder'; + | 'useResponder' + | 'useDeferredValue' + | 'useTransition'; let didWarnAboutMismatchedHooksForComponent; if (__DEV__) { @@ -152,6 +159,10 @@ export type FunctionComponentUpdateQueue = { lastEffect: Effect | null, }; +export type TimeoutConfig = {| + timeoutMs: number, +|}; + type BasicStateAction = (S => S) | S; type Dispatch = A => void; @@ -1117,6 +1128,96 @@ function updateMemo( return nextValue; } +function mountDeferredValue( + value: T, + config: TimeoutConfig | void | null, +): T { + const [prevValue, setValue] = mountState(value); + mountEffect( + () => { + Scheduler.unstable_next(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setValue(value); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }); + }, + [value, config], + ); + return prevValue; +} + +function updateDeferredValue( + value: T, + config: TimeoutConfig | void | null, +): T { + const [prevValue, setValue] = updateState(value); + updateEffect( + () => { + Scheduler.unstable_next(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setValue(value); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }); + }, + [value, config], + ); + return prevValue; +} + +function mountTransition( + config: SuspenseConfig | void | null, +): [(() => void) => void, boolean] { + const [isPending, setPending] = mountState(false); + const startTransition = mountCallback( + callback => { + setPending(true); + Scheduler.unstable_next(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setPending(false); + callback(); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }); + }, + [config, isPending], + ); + return [startTransition, isPending]; +} + +function updateTransition( + config: SuspenseConfig | void | null, +): [(() => void) => void, boolean] { + const [isPending, setPending] = updateState(false); + const startTransition = updateCallback( + callback => { + setPending(true); + Scheduler.unstable_next(() => { + const previousConfig = ReactCurrentBatchConfig.suspense; + ReactCurrentBatchConfig.suspense = config === undefined ? null : config; + try { + setPending(false); + callback(); + } finally { + ReactCurrentBatchConfig.suspense = previousConfig; + } + }); + }, + [config, isPending], + ); + return [startTransition, isPending]; +} + function dispatchAction( fiber: Fiber, queue: UpdateQueue, @@ -1272,6 +1373,8 @@ export const ContextOnlyDispatcher: Dispatcher = { useState: throwInvalidHookError, useDebugValue: throwInvalidHookError, useResponder: throwInvalidHookError, + useDeferredValue: throwInvalidHookError, + useTransition: throwInvalidHookError, }; const HooksDispatcherOnMount: Dispatcher = { @@ -1288,6 +1391,8 @@ const HooksDispatcherOnMount: Dispatcher = { useState: mountState, useDebugValue: mountDebugValue, useResponder: createResponderListener, + useDeferredValue: mountDeferredValue, + useTransition: mountTransition, }; const HooksDispatcherOnUpdate: Dispatcher = { @@ -1304,6 +1409,8 @@ const HooksDispatcherOnUpdate: Dispatcher = { useState: updateState, useDebugValue: updateDebugValue, useResponder: createResponderListener, + useDeferredValue: updateDeferredValue, + useTransition: updateTransition, }; let HooksDispatcherOnMountInDEV: Dispatcher | null = null; @@ -1441,6 +1548,18 @@ if (__DEV__) { mountHookTypesDev(); return createResponderListener(responder, props); }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + mountHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + mountHookTypesDev(); + return mountTransition(config); + }, }; HooksDispatcherOnMountWithHookTypesInDEV = { @@ -1546,6 +1665,18 @@ if (__DEV__) { updateHookTypesDev(); return createResponderListener(responder, props); }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + updateHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + updateHookTypesDev(); + return mountTransition(config); + }, }; HooksDispatcherOnUpdateInDEV = { @@ -1651,6 +1782,18 @@ if (__DEV__) { updateHookTypesDev(); return createResponderListener(responder, props); }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + updateHookTypesDev(); + return updateDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + updateHookTypesDev(); + return updateTransition(config); + }, }; InvalidNestedHooksDispatcherOnMountInDEV = { @@ -1768,6 +1911,20 @@ if (__DEV__) { mountHookTypesDev(); return createResponderListener(responder, props); }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountTransition(config); + }, }; InvalidNestedHooksDispatcherOnUpdateInDEV = { @@ -1885,5 +2042,19 @@ if (__DEV__) { updateHookTypesDev(); return createResponderListener(responder, props); }, + useDeferredValue(value: T, config: TimeoutConfig | void | null): T { + currentHookNameInDev = 'useDeferredValue'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDeferredValue(value, config); + }, + useTransition( + config: SuspenseConfig | void | null, + ): [(() => void) => void, boolean] { + currentHookNameInDev = 'useTransition'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateTransition(config); + }, }; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 63c7eefca591d..898457308af68 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -2930,7 +2930,7 @@ function flushSuspensePriorityWarningInDEV() { 'update to provide immediate feedback, and another update that ' + 'triggers the bulk of the changes.' + '\n\n' + - 'Refer to the documentation for useSuspenseTransition to learn how ' + + 'Refer to the documentation for useTransition to learn how ' + 'to implement this pattern.', // TODO: Add link to React docs with more information, once it exists componentNames.sort().join(', '), diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 7bece8ee1aef5..6bc4871e4c685 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1485,6 +1485,31 @@ describe('ReactHooks', () => { useStateHelper, ]; + // We don't include useContext or useDebugValue in this set, + // because they aren't added to the hooks list and so won't throw. + let hooksInList = [ + useCallbackHelper, + useEffectHelper, + useImperativeHandleHelper, + useLayoutEffectHelper, + useMemoHelper, + useReducerHelper, + useRefHelper, + useStateHelper, + ]; + + if (__EXPERIMENTAL__) { + const useTransitionHelper = () => React.useTransition({timeoutMs: 1000}); + const useDeferredValueHelper = () => + React.useDeferredValue(0, {timeoutMs: 1000}); + + orderedHooks.push(useTransitionHelper); + orderedHooks.push(useDeferredValueHelper); + + hooksInList.push(useTransitionHelper); + hooksInList.push(useDeferredValueHelper); + } + const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => { return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${ hookNameB ? `use${hookNameB}` : undefined @@ -1598,19 +1623,6 @@ describe('ReactHooks', () => { }); }); - // We don't include useContext or useDebugValue in this set, - // because they aren't added to the hooks list and so won't throw. - let hooksInList = [ - useCallbackHelper, - useEffectHelper, - useImperativeHandleHelper, - useLayoutEffectHelper, - useMemoHelper, - useReducerHelper, - useRefHelper, - useStateHelper, - ]; - hooksInList.forEach((firstHelper, index) => { const secondHelper = index > 0 diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 151688592507f..4cd85e6e85d5b 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -13,10 +13,13 @@ 'use strict'; let React; +let ReactCache; +let TextResource; let ReactFeatureFlags; let ReactNoop; let Scheduler; let SchedulerTracing; +let Suspense; let useState; let useReducer; let useEffect; @@ -25,6 +28,8 @@ let useCallback; let useMemo; let useRef; let useImperativeHandle; +let useTransition; +let useDeferredValue; let forwardRef; let memo; let act; @@ -32,14 +37,17 @@ let act; describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { jest.resetModules(); + jest.useFakeTimers(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableSchedulerTracing = true; + ReactFeatureFlags.flushSuspenseFallbacksInTests = false; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); SchedulerTracing = require('scheduler/tracing'); + ReactCache = require('react-cache'); useState = React.useState; useReducer = React.useReducer; useEffect = React.useEffect; @@ -50,18 +58,62 @@ describe('ReactHooksWithNoopRenderer', () => { useImperativeHandle = React.useImperativeHandle; forwardRef = React.forwardRef; memo = React.memo; + useTransition = React.useTransition; + useDeferredValue = React.useDeferredValue; + Suspense = React.Suspense; act = ReactNoop.act; + + TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => { + return new Promise((resolve, reject) => + setTimeout(() => { + Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); + resolve(text); + }, ms), + ); + }, ([text, ms]) => text); }); function span(prop) { return {type: 'span', hidden: false, children: [], prop}; } + function hiddenSpan(prop) { + return {type: 'span', children: [], prop, hidden: true}; + } + function Text(props) { Scheduler.unstable_yieldValue(props.text); return ; } + function AsyncText(props) { + const text = props.text; + try { + TextResource.read([props.text, props.ms]); + Scheduler.unstable_yieldValue(text); + return ; + } catch (promise) { + if (typeof promise.then === 'function') { + Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + } else { + Scheduler.unstable_yieldValue(`Error! [${text}]`); + } + throw promise; + } + } + + function advanceTimers(ms) { + // Note: This advances Jest's virtual time but not React's. Use + // ReactNoop.expire for that. + if (typeof ms !== 'number') { + throw new Error('Must specify ms'); + } + jest.advanceTimersByTime(ms); + // Wait until the end of the current tick + // We cannot use a timer since we're faking them + return Promise.resolve().then(() => {}); + } + it('resumes after an interruption', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); @@ -1921,7 +1973,213 @@ describe('ReactHooksWithNoopRenderer', () => { expect(totalRefUpdates).toBe(2); // Should not increase since last time }); }); + if (__EXPERIMENTAL__) { + describe('useTransition', () => { + it('delays showing loading state until after timeout', async () => { + let transition; + function App() { + const [show, setShow] = useState(false); + const [startTransition, isPending] = useTransition({ + timeoutMs: 1000, + }); + transition = () => { + startTransition(() => { + setShow(true); + }); + }; + return ( + }> + {show ? ( + + ) : ( + + )} + + ); + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Before... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: false'), + ]); + + act(() => { + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + transition, + ); + }); + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(Scheduler).toHaveYielded([ + 'Before... Pending: true', + 'Suspend! [After... Pending: false]', + 'Loading... Pending: false', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(ReactNoop.getChildren()).toEqual([ + hiddenSpan('Before... Pending: true'), + span('Loading... Pending: false'), + ]); + + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [After... Pending: false]', + ]); + expect(Scheduler).toFlushAndYield(['After... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('After... Pending: false'), + ]); + }); + it('delays showing loading state until after busyDelayMs + busyMinDurationMs', async () => { + let transition; + function App() { + const [show, setShow] = useState(false); + const [startTransition, isPending] = useTransition({ + busyDelayMs: 1000, + busyMinDurationMs: 2000, + }); + transition = () => { + startTransition(() => { + setShow(true); + }); + }; + return ( + }> + {show ? ( + + ) : ( + + )} + + ); + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Before... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: false'), + ]); + + act(() => { + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + transition, + ); + }); + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded([ + 'Before... Pending: true', + 'Suspend! [After... Pending: false]', + 'Loading... Pending: false', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [After... Pending: false]', + ]); + expect(Scheduler).toFlushAndYield(['After... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(ReactNoop.getChildren()).toEqual([ + span('After... Pending: false'), + ]); + }); + }); + describe('useDeferredValue', () => { + it('defers text value until specified timeout', async () => { + function TextBox({text}) { + return ; + } + + let _setText; + function App() { + const [text, setText] = useState('A'); + const deferredText = useDeferredValue(text, { + timeoutMs: 500, + }); + _setText = setText; + return ( + <> + + }> + + + + ); + } + + act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]); + + act(() => { + _setText('B'); + }); + expect(Scheduler).toHaveYielded([ + 'B', + 'A', + 'B', + 'Suspend! [B]', + 'Loading', + ]); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); + + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); + + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(ReactNoop.getChildren()).toEqual([ + span('B'), + hiddenSpan('A'), + span('Loading'), + ]); + + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + act(() => { + expect(Scheduler).toFlushAndYield(['B']); + }); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]); + }); + }); + } describe('progressive enhancement (not supported)', () => { it('mount additional state', () => { let updateA; diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 50897342d92e5..e4e32bd00e359 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -377,6 +377,23 @@ class ReactShallowRenderer { responder, }); + // TODO: implement if we decide to keep the shallow renderer + const useTransition = ( + config, + ): [(callback: () => void) => void, boolean] => { + this._validateCurrentlyRenderingComponent(); + const startTransition = callback => { + callback(); + }; + return [startTransition, false]; + }; + + // TODO: implement if we decide to keep the shallow renderer + const useDeferredValue = (value: T, config): T => { + this._validateCurrentlyRenderingComponent(); + return value; + }; + return { readContext, useCallback: (identity: any), @@ -393,6 +410,8 @@ class ReactShallowRenderer { useRef, useState, useResponder, + useTransition, + useDeferredValue, }; } diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 112ff1620b376..bfae007942bdc 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -40,6 +40,8 @@ import { useRef, useState, useResponder, + useTransition, + useDeferredValue, } from './ReactHooks'; import {withSuspenseConfig} from './ReactBatchConfig'; import { @@ -59,6 +61,7 @@ import { enableFlareAPI, enableFundamentalAPI, enableScopeAPI, + exposeConcurrentModeAPIs, } from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -107,6 +110,11 @@ const React = { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals, }; +if (exposeConcurrentModeAPIs) { + React.useTransition = useTransition; + React.useDeferredValue = useDeferredValue; +} + if (enableFlareAPI) { React.unstable_useResponder = useResponder; React.unstable_createResponder = createResponder; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1439500670b46..6072720cdc814 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -160,3 +160,15 @@ export function useResponder( } return dispatcher.useResponder(responder, listenerProps || emptyObject); } + +export function useTransition( + config: ?Object, +): [(() => void) => void, boolean] { + const dispatcher = resolveDispatcher(); + return dispatcher.useTransition(config); +} + +export function useDeferredValue(value: T, config: ?Object): T { + const dispatcher = resolveDispatcher(); + return dispatcher.useDeferredValue(value, config); +}