diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
index 10a49b447d9e0..0e4bf4669ec8d 100644
--- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
@@ -31,6 +31,8 @@ import type {
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
+import type {FormStatus} from '../shared/ReactDOMFormActions';
+
import {
writeChunk,
writeChunkAndReturn,
@@ -82,6 +84,8 @@ import {
describeDifferencesForPreloadOverImplicitPreload,
} from '../shared/ReactDOMResourceValidation';
+import {NotPending} from '../shared/ReactDOMFormActions';
+
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
@@ -5562,3 +5566,6 @@ function getAsResourceDEV(
);
}
}
+
+export type TransitionStatus = FormStatus;
+export const NotPendingTransition: TransitionStatus = NotPending;
diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
index 4feafb782dae6..474921d69a5e5 100644
--- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
@@ -31,6 +31,10 @@ import type {
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
+import type {FormStatus} from '../shared/ReactDOMFormActions';
+
+import {NotPending} from '../shared/ReactDOMFormActions';
+
export const isPrimaryRenderer = false;
export type ResponseState = {
@@ -226,3 +230,6 @@ export function writeEndClientRenderedSuspenseBoundary(
}
return writeEndClientRenderedSuspenseBoundaryImpl(destination, responseState);
}
+
+export type TransitionStatus = FormStatus;
+export const NotPendingTransition: TransitionStatus = NotPending;
diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
index bd98a29c17b49..d716782e709d8 100644
--- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
+++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
@@ -7,7 +7,12 @@
* @flow
*/
+import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
+
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
+import ReactSharedInternals from 'shared/ReactSharedInternals';
+
+const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
type FormStatusNotPending = {|
pending: false,
@@ -38,13 +43,34 @@ export const NotPending: FormStatus = __DEV__
? Object.freeze(sharedNotPendingObject)
: sharedNotPendingObject;
+function resolveDispatcher() {
+ // Copied from react/src/ReactHooks.js. It's the same thing but in a
+ // different package.
+ const dispatcher = ReactCurrentDispatcher.current;
+ if (__DEV__) {
+ if (dispatcher === null) {
+ console.error(
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
+ ' one of the following reasons:\n' +
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
+ '2. You might be breaking the Rules of Hooks\n' +
+ '3. You might have more than one copy of React in the same app\n' +
+ 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
+ );
+ }
+ }
+ // Will result in a null access error if accessed outside render phase. We
+ // intentionally don't throw our own error because this is in a hot path.
+ // Also helps ensure this is inlined.
+ return ((dispatcher: any): Dispatcher);
+}
+
export function useFormStatus(): FormStatus {
if (!(enableFormActions && enableAsyncActions)) {
throw new Error('Not implemented.');
} else {
- // TODO: This isn't fully implemented yet but we return a correctly typed
- // value so we can test that the API is exposed and gated correctly. The
- // real implementation will access the status via the dispatcher.
- return NotPending;
+ const dispatcher = resolveDispatcher();
+ // $FlowFixMe We know this exists because of the feature check above.
+ return dispatcher.useHostTransitionStatus();
}
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
index ba312e9dd66d9..50ef3d0212875 100644
--- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
@@ -647,10 +647,16 @@ describe('ReactDOMForm', () => {
it('form actions are transitions', async () => {
const formRef = React.createRef();
+ function Status() {
+ const {pending} = useFormStatus();
+ return pending ? : null;
+ }
+
function App() {
const [state, setState] = useState('Initial');
return (
+ );
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render());
- expect(container.textContent).toBe('Pending: false');
+ assertLog(['No pending action']);
+ expect(container.textContent).toBe('No pending action');
+
+ await submit(formRef.current);
+ assertLog([
+ 'Async action started',
+ 'Pending action myAction: foo is bar, method is get',
+ ]);
+ expect(container.textContent).toBe(
+ 'Pending action myAction: foo is bar, method is get',
+ );
+
+ await act(() => resolveText('Wait'));
+ assertLog(['Async action finished', 'No pending action']);
});
});
diff --git a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js
index 4e348d1e8ac20..61f746b4b987d 100644
--- a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js
+++ b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js
@@ -354,3 +354,6 @@ export function writeResourcesForBoundary(
): boolean {
return true;
}
+
+export type TransitionStatus = mixed;
+export const NotPendingTransition: TransitionStatus = null;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index ad408f474080f..f237a26f90706 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -38,6 +38,8 @@ import type {
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
import type {RootState} from './ReactFiberRoot';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
+import type {TransitionStatus} from './ReactFiberConfig';
+import type {Hook} from './ReactFiberHooks';
import checkPropTypes from 'shared/checkPropTypes';
import {
@@ -176,6 +178,7 @@ import {
pushHostContext,
pushHostContainer,
getRootHostContainer,
+ HostTransitionContext,
} from './ReactFiberHostContext';
import {
suspenseStackCursor,
@@ -1632,11 +1635,49 @@ function updateHostComponent(
//
// Once a fiber is upgraded to be stateful, it remains stateful for the
// rest of its lifetime.
- renderTransitionAwareHostComponentWithHooks(
+ const newState = renderTransitionAwareHostComponentWithHooks(
current,
workInProgress,
renderLanes,
);
+
+ // If the transition state changed, propagate the change to all the
+ // descendents. We use Context as an implementation detail for this.
+ //
+ // This is intentionally set here instead of pushHostContext because
+ // pushHostContext gets called before we process the state hook, to avoid
+ // a state mismatch in the event that something suspends.
+ //
+ // NOTE: This assumes that there cannot be nested transition providers,
+ // because the only renderer that implements this feature is React DOM,
+ // and forms cannot be nested. If we did support nested providers, then
+ // we would need to push a context value even for host fibers that
+ // haven't been upgraded yet.
+ if (isPrimaryRenderer) {
+ HostTransitionContext._currentValue = newState;
+ } else {
+ HostTransitionContext._currentValue2 = newState;
+ }
+ if (enableLazyContextPropagation) {
+ // In the lazy propagation implementation, we don't scan for matching
+ // consumers until something bails out.
+ } else {
+ if (didReceiveUpdate) {
+ if (current !== null) {
+ const oldStateHook: Hook = current.memoizedState;
+ const oldState: TransitionStatus = oldStateHook.memoizedState;
+ // This uses regular equality instead of Object.is because we assume
+ // that host transition state doesn't include NaN as a valid type.
+ if (oldState !== newState) {
+ propagateContextChange(
+ workInProgress,
+ HostTransitionContext,
+ renderLanes,
+ );
+ }
+ }
+ }
+ }
}
}
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index bd9bf2c747b0e..10ae6565e6b2f 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -148,6 +148,7 @@ import {
import type {ThenableState} from './ReactFiberThenable';
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
import {requestAsyncActionContext} from './ReactFiberAsyncAction';
+import {HostTransitionContext} from './ReactFiberHostContext';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -2645,6 +2646,14 @@ function rerenderTransition(): [
return [isPending, start];
}
+function useHostTransitionStatus(): TransitionStatus {
+ if (!(enableFormActions && enableAsyncActions)) {
+ throw new Error('Not implemented.');
+ }
+ const status: TransitionStatus | null = readContext(HostTransitionContext);
+ return status !== null ? status : NoPendingHostTransition;
+}
+
function mountId(): string {
const hook = mountWorkInProgressHook();
@@ -2972,6 +2981,10 @@ if (enableUseMemoCacheHook) {
if (enableUseEffectEventHook) {
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
}
+if (enableFormActions && enableAsyncActions) {
+ (ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
+ throwInvalidHookError;
+}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@@ -3003,6 +3016,10 @@ if (enableUseMemoCacheHook) {
if (enableUseEffectEventHook) {
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
}
+if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -3033,6 +3050,10 @@ if (enableUseMemoCacheHook) {
if (enableUseEffectEventHook) {
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
}
+if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -3064,6 +3085,10 @@ if (enableUseMemoCacheHook) {
if (enableUseEffectEventHook) {
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
}
+if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -3250,6 +3275,10 @@ if (__DEV__) {
return mountEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext(context: ReactContext): T {
@@ -3404,6 +3433,10 @@ if (__DEV__) {
return mountEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
HooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -3560,6 +3593,10 @@ if (__DEV__) {
return updateEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
HooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -3716,6 +3753,10 @@ if (__DEV__) {
return updateEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext(context: ReactContext): T {
@@ -3894,6 +3935,10 @@ if (__DEV__) {
return mountEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext(context: ReactContext): T {
@@ -4075,6 +4120,10 @@ if (__DEV__) {
return updateEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext(context: ReactContext): T {
@@ -4256,4 +4305,8 @@ if (__DEV__) {
return updateEvent(callback);
};
}
+ if (enableFormActions && enableAsyncActions) {
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
+ useHostTransitionStatus;
+ }
}
diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js
index c5733b24543d2..d909002dc97f2 100644
--- a/packages/react-reconciler/src/ReactFiberHostContext.js
+++ b/packages/react-reconciler/src/ReactFiberHostContext.js
@@ -9,16 +9,52 @@
import type {Fiber} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack';
-import type {Container, HostContext} from './ReactFiberConfig';
-
-import {getChildHostContext, getRootHostContext} from './ReactFiberConfig';
+import type {
+ Container,
+ HostContext,
+ TransitionStatus,
+} from './ReactFiberConfig';
+import type {Hook} from './ReactFiberHooks';
+import type {ReactContext} from 'shared/ReactTypes';
+
+import {
+ getChildHostContext,
+ getRootHostContext,
+ isPrimaryRenderer,
+} from './ReactFiberConfig';
import {createCursor, push, pop} from './ReactFiberStack';
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
+import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
const contextStackCursor: StackCursor = createCursor(null);
const contextFiberStackCursor: StackCursor = createCursor(null);
const rootInstanceStackCursor: StackCursor =
createCursor(null);
+// Represents the nearest host transition provider (in React DOM, a )
+// NOTE: Since forms cannot be nested, and this feature is only implemented by
+// React DOM, we don't technically need this to be a stack. It could be a single
+// module variable instead.
+const hostTransitionProviderCursor: StackCursor =
+ createCursor(null);
+
+// TODO: This should initialize to NotPendingTransition, a constant
+// imported from the fiber config. However, because of a cycle in the module
+// graph, that value isn't defined during this module's initialization. I can't
+// think of a way to work around this without moving that value out of the
+// fiber config. For now, the "no provider" case is handled when reading,
+// inside useHostTransitionStatus.
+export const HostTransitionContext: ReactContext = {
+ $$typeof: REACT_CONTEXT_TYPE,
+ _currentValue: null,
+ _currentValue2: null,
+ _threadCount: 0,
+ Provider: (null: any),
+ Consumer: (null: any),
+ _defaultValue: (null: any),
+ _globalName: (null: any),
+};
+
function requiredContext(c: Value | null): Value {
if (__DEV__) {
if (c === null) {
@@ -40,6 +76,10 @@ function getRootHostContainer(): Container {
return rootInstance;
}
+export function getHostTransitionProvider(): Fiber | null {
+ return hostTransitionProviderCursor.current;
+}
+
function pushHostContainer(fiber: Fiber, nextRootInstance: Container): void {
// Push current root instance onto the stack;
// This allows us to reset root when portals are popped.
@@ -72,29 +112,56 @@ function getHostContext(): HostContext {
}
function pushHostContext(fiber: Fiber): void {
+ if (enableFormActions && enableAsyncActions) {
+ const stateHook: Hook | null = fiber.memoizedState;
+ if (stateHook !== null) {
+ // Only provide context if this fiber has been upgraded by a host
+ // transition. We use the same optimization for regular host context below.
+ push(hostTransitionProviderCursor, fiber, fiber);
+ }
+ }
+
const context: HostContext = requiredContext(contextStackCursor.current);
const nextContext = getChildHostContext(context, fiber.type);
// Don't push this Fiber's context unless it's unique.
- if (context === nextContext) {
- return;
+ if (context !== nextContext) {
+ // Track the context and the Fiber that provided it.
+ // This enables us to pop only Fibers that provide unique contexts.
+ push(contextFiberStackCursor, fiber, fiber);
+ push(contextStackCursor, nextContext, fiber);
}
-
- // Track the context and the Fiber that provided it.
- // This enables us to pop only Fibers that provide unique contexts.
- push(contextFiberStackCursor, fiber, fiber);
- push(contextStackCursor, nextContext, fiber);
}
function popHostContext(fiber: Fiber): void {
- // Do not pop unless this Fiber provided the current context.
- // pushHostContext() only pushes Fibers that provide unique contexts.
- if (contextFiberStackCursor.current !== fiber) {
- return;
+ if (contextFiberStackCursor.current === fiber) {
+ // Do not pop unless this Fiber provided the current context.
+ // pushHostContext() only pushes Fibers that provide unique contexts.
+ pop(contextStackCursor, fiber);
+ pop(contextFiberStackCursor, fiber);
}
- pop(contextStackCursor, fiber);
- pop(contextFiberStackCursor, fiber);
+ if (enableFormActions && enableAsyncActions) {
+ if (hostTransitionProviderCursor.current === fiber) {
+ // Do not pop unless this Fiber provided the current context. This is mostly
+ // a performance optimization, but conveniently it also prevents a potential
+ // data race where a host provider is upgraded (i.e. memoizedState becomes
+ // non-null) during a concurrent event. This is a bit of a flaw in the way
+ // we upgrade host components, but because we're accounting for it here, it
+ // should be fine.
+ pop(hostTransitionProviderCursor, fiber);
+
+ // When popping the transition provider, we reset the context value back
+ // to `null`. We can do this because you're not allowd to nest forms. If
+ // we allowed for multiple nested host transition providers, then we'd
+ // need to reset this to the parent provider's status.
+ if (isPrimaryRenderer) {
+ HostTransitionContext._currentValue = null;
+ } else {
+ HostTransitionContext._currentValue2 = null;
+ }
+ }
+ }
}
export {
diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js
index 003022a9c6265..d4f4eb048e43b 100644
--- a/packages/react-reconciler/src/ReactFiberNewContext.js
+++ b/packages/react-reconciler/src/ReactFiberNewContext.js
@@ -16,6 +16,8 @@ import type {
import type {StackCursor} from './ReactFiberStack';
import type {Lanes} from './ReactFiberLane';
import type {SharedQueue} from './ReactFiberClassUpdateQueue';
+import type {TransitionStatus} from './ReactFiberConfig';
+import type {Hook} from './ReactFiberHooks';
import {isPrimaryRenderer} from './ReactFiberConfig';
import {createCursor, push, pop} from './ReactFiberStack';
@@ -43,8 +45,14 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
import {
enableLazyContextPropagation,
enableServerContext,
+ enableFormActions,
+ enableAsyncActions,
} from 'shared/ReactFeatureFlags';
import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols';
+import {
+ getHostTransitionProvider,
+ HostTransitionContext,
+} from './ReactFiberHostContext';
const valueCursor: StackCursor = createCursor(null);
@@ -585,6 +593,33 @@ function propagateParentContextChanges(
}
}
}
+ } else if (
+ enableFormActions &&
+ enableAsyncActions &&
+ parent === getHostTransitionProvider()
+ ) {
+ // During a host transition, a host component can act like a context
+ // provider. E.g. in React DOM, this would be a .
+ const currentParent = parent.alternate;
+ if (currentParent === null) {
+ throw new Error('Should have a current fiber. This is a bug in React.');
+ }
+
+ const oldStateHook: Hook = currentParent.memoizedState;
+ const oldState: TransitionStatus = oldStateHook.memoizedState;
+
+ const newStateHook: Hook = parent.memoizedState;
+ const newState: TransitionStatus = newStateHook.memoizedState;
+
+ // This uses regular equality instead of Object.is because we assume that
+ // host transition state doesn't include NaN as a valid type.
+ if (oldState !== newState) {
+ if (contexts !== null) {
+ contexts.push(HostTransitionContext);
+ } else {
+ contexts = [HostTransitionContext];
+ }
+ }
}
parent = parent.return;
}
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 1421181cb8ea9..bc9cfd1fb307f 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -29,6 +29,7 @@ import type {
TimeoutHandle,
NoTimeout,
SuspenseInstance,
+ TransitionStatus,
} from './ReactFiberConfig';
import type {Cache} from './ReactFiberCacheComponent';
import type {
@@ -421,6 +422,9 @@ export type Dispatcher = {
useId(): string,
useCacheRefresh?: () => (?() => T, ?T) => void,
useMemoCache?: (size: number) => Array,
+ useHostTransitionStatus?: (
+ initialStatus: TransitionStatus,
+ ) => TransitionStatus,
};
export type CacheDispatcher = {
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 62e4586f74590..332c1721d5e6e 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -22,17 +22,20 @@ import type {
import type {ResponseState} from './ReactFizzConfig';
import type {Task} from './ReactFizzServer';
import type {ThenableState} from './ReactFizzThenable';
+import type {TransitionStatus} from './ReactFizzConfig';
import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext';
import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
-import {makeId} from './ReactFizzConfig';
+import {makeId, NotPendingTransition} from './ReactFizzConfig';
import {
enableCache,
enableUseEffectEventHook,
enableUseMemoCacheHook,
+ enableAsyncActions,
+ enableFormActions,
} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';
import {
@@ -545,6 +548,11 @@ function useTransition(): [
return [false, unsupportedStartTransition];
}
+function useHostTransitionStatus(): TransitionStatus {
+ resolveCurrentlyRenderingComponent();
+ return NotPendingTransition;
+}
+
function useId(): string {
const task: Task = (currentlyRenderingTask: any);
const treeId = getTreeId(task.treeContext);
@@ -641,6 +649,9 @@ if (enableUseEffectEventHook) {
if (enableUseMemoCacheHook) {
HooksDispatcher.useMemoCache = useMemoCache;
}
+if (enableFormActions && enableAsyncActions) {
+ HooksDispatcher.useHostTransitionStatus = useHostTransitionStatus;
+}
export let currentResponseState: null | ResponseState = (null: any);
export function setCurrentResponseState(
diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js
index 4b44462d9d412..1c55f57a00e3d 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.custom.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js
@@ -32,6 +32,7 @@ export opaque type Resources = mixed;
export opaque type BoundaryResources = mixed;
export opaque type FormatContext = mixed;
export opaque type SuspenseBoundaryID = mixed;
+export opaque type TransitionStatus = mixed;
export const isPrimaryRenderer = false;
@@ -74,6 +75,7 @@ export const writeCompletedBoundaryInstruction =
export const writeClientRenderBoundaryInstruction =
$$$config.writeClientRenderBoundaryInstruction;
export const prepareHostDispatcher = $$$config.prepareHostDispatcher;
+export const NotPendingTransition = $$$config.NotPendingTransition;
// -------------------------
// Resources