From ef63718a27407b6d6b262d6be92e6bf0a87ff1a3 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 13 Dec 2024 13:58:18 -0500 Subject: [PATCH 1/6] Remove enableAsyncActions (#31757) Based on https://github.com/facebook/react/pull/31756 This is landed everywhere --- .../react-debug-tools/src/ReactDebugHooks.js | 21 +- .../ReactHooksInspectionIntegration-test.js | 2 - .../src/client/ReactFiberConfigDOM.js | 6 +- .../src/shared/ReactDOMFormActions.js | 19 +- .../src/__tests__/ReactDOMFizzForm-test.js | 3 - .../src/__tests__/ReactDOMFizzServer-test.js | 2 - .../src/__tests__/ReactDOMForm-test.js | 25 - packages/react-dom/src/client/ReactDOMRoot.js | 7 +- .../src/ReactFiberBeginWork.js | 95 ++- .../react-reconciler/src/ReactFiberHooks.js | 664 +++++++----------- .../src/ReactFiberHostContext.js | 49 +- .../src/ReactFiberNewContext.js | 3 +- .../src/ReactFiberTransition.js | 7 +- .../src/ReactInternalTypes.js | 8 +- .../src/__tests__/ReactAsyncActions-test.js | 65 +- .../src/__tests__/ReactFlightDOMForm-test.js | 10 +- packages/react-server/src/ReactFizzHooks.js | 17 +- packages/react-server/src/ReactFlightHooks.js | 4 + packages/react/src/ReactHooks.js | 15 +- packages/react/src/ReactStartTransition.js | 48 +- packages/shared/ReactFeatureFlags.js | 1 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 2 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 2 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 27 files changed, 394 insertions(+), 685 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 2a0c57154e6f9..eccaa6a67b166 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -104,22 +104,14 @@ function getPrimitiveStackCache(): Map> { ); Dispatcher.useDeferredValue(null); Dispatcher.useMemo(() => null); + Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); + Dispatcher.useFormState((s: mixed, p: mixed) => s, null); + Dispatcher.useActionState((s: mixed, p: mixed) => s, null); + Dispatcher.useHostTransitionStatus(); if (typeof Dispatcher.useMemoCache === 'function') { // This type check is for Flow only. Dispatcher.useMemoCache(0); } - if (typeof Dispatcher.useOptimistic === 'function') { - // This type check is for Flow only. - Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); - } - if (typeof Dispatcher.useFormState === 'function') { - // This type check is for Flow only. - Dispatcher.useFormState((s: mixed, p: mixed) => s, null); - } - if (typeof Dispatcher.useActionState === 'function') { - // This type check is for Flow only. - Dispatcher.useActionState((s: mixed, p: mixed) => s, null); - } if (typeof Dispatcher.use === 'function') { // This type check is for Flow only. Dispatcher.use( @@ -143,11 +135,6 @@ function getPrimitiveStackCache(): Map> { } Dispatcher.useId(); - - if (typeof Dispatcher.useHostTransitionStatus === 'function') { - // This type check is for Flow only. - Dispatcher.useHostTransitionStatus(); - } } finally { readHookLog = hookLog; hookLog = []; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index ef9a7c1e6edc7..fcd84fedfed71 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -2581,7 +2581,6 @@ describe('ReactHooksInspectionIntegration', () => { `); }); - // @gate enableAsyncActions it('should support useOptimistic hook', async () => { const useOptimistic = React.useOptimistic; function Foo() { @@ -2647,7 +2646,6 @@ describe('ReactHooksInspectionIntegration', () => { `); }); - // @gate enableAsyncActions it('should support useActionState hook', async () => { function Foo() { const [value] = React.useActionState(function increment(n) { diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 0933794c68cb6..8e94d48beeef2 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -91,7 +91,6 @@ import { enableCreateEventHandleAPI, enableScopeAPI, enableTrustedTypesIntegration, - enableAsyncActions, disableLegacyMode, enableMoveBefore, } from 'shared/ReactFeatureFlags'; @@ -1378,9 +1377,8 @@ function getNextHydratable(node: ?Node) { nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || nodeData === SUSPENSE_PENDING_START_DATA || - (enableAsyncActions && - (nodeData === FORM_STATE_IS_MATCHING || - nodeData === FORM_STATE_IS_NOT_MATCHING)) + nodeData === FORM_STATE_IS_MATCHING || + nodeData === FORM_STATE_IS_NOT_MATCHING ) { break; } diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js index ab36fc11199b1..6dd4e4da44479 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js @@ -10,7 +10,6 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type {Awaited} from 'shared/ReactTypes'; -import {enableAsyncActions} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; @@ -66,13 +65,8 @@ function resolveDispatcher() { } export function useFormStatus(): FormStatus { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } else { - const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] We know this exists because of the feature check above. - return dispatcher.useHostTransitionStatus(); - } + const dispatcher = resolveDispatcher(); + return dispatcher.useHostTransitionStatus(); } export function useFormState( @@ -80,13 +74,8 @@ export function useFormState( initialState: Awaited, permalink?: string, ): [Awaited, (P) => void, boolean] { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } else { - const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional - return dispatcher.useFormState(action, initialState, permalink); - } + const dispatcher = resolveDispatcher(); + return dispatcher.useFormState(action, initialState, permalink); } export function requestFormReset(form: HTMLFormElement) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index 0ecfe3eb82e0b..2300f4563c4b6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -398,7 +398,6 @@ describe('ReactDOMFizzForm', () => { expect(buttonRef.current.hasAttribute('formTarget')).toBe(false); }); - // @gate enableAsyncActions it('useFormStatus is not pending during server render', async () => { function App() { const {pending} = useFormStatus(); @@ -488,7 +487,6 @@ describe('ReactDOMFizzForm', () => { expect(rootActionCalled).toBe(false); }); - // @gate enableAsyncActions it('useOptimistic returns passthrough value', async () => { function App() { const [optimisticState] = useOptimistic('hi'); @@ -507,7 +505,6 @@ describe('ReactDOMFizzForm', () => { expect(container.textContent).toBe('hi'); }); - // @gate enableAsyncActions it('useActionState returns initial state', async () => { async function action(state) { return state; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 8a8619ff87e7a..9c91be1225015 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -6330,7 +6330,6 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual('Hi'); }); - // @gate enableAsyncActions it('useActionState hydrates without a mismatch', async () => { // This is testing an implementation detail: useActionState emits comment // nodes into the SSR stream, so this checks that they are handled correctly @@ -6383,7 +6382,6 @@ describe('ReactDOMFizzServer', () => { expect(childRef.current).toBe(child); }); - // @gate enableAsyncActions it("useActionState hydrates without a mismatch if there's a render phase update", async () => { async function action(state) { return state; diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 168d44722d473..7185cc5cad152 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -669,7 +669,6 @@ describe('ReactDOMForm', () => { expect(actionCalled).toBe(false); }); - // @gate enableAsyncActions it('form actions are transitions', async () => { const formRef = React.createRef(); @@ -707,7 +706,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('Updated'); }); - // @gate enableAsyncActions it('multiple form actions', async () => { const formRef = React.createRef(); @@ -798,12 +796,6 @@ describe('ReactDOMForm', () => { }); it('sync errors in form actions can be captured by an error boundary', async () => { - if (gate(flags => !flags.enableAsyncActions)) { - // TODO: Uncaught JSDOM errors fail the test after the scope has finished - // so don't work with the `gate` mechanism. - return; - } - class ErrorBoundary extends React.Component { state = {error: null}; static getDerivedStateFromError(error) { @@ -844,12 +836,6 @@ describe('ReactDOMForm', () => { }); it('async errors in form actions can be captured by an error boundary', async () => { - if (gate(flags => !flags.enableAsyncActions)) { - // TODO: Uncaught JSDOM errors fail the test after the scope has finished - // so don't work with the `gate` mechanism. - return; - } - class ErrorBoundary extends React.Component { state = {error: null}; static getDerivedStateFromError(error) { @@ -895,7 +881,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('Oh no!'); }); - // @gate enableAsyncActions it('useFormStatus reads the status of a pending form action', async () => { const formRef = React.createRef(); @@ -992,7 +977,6 @@ describe('ReactDOMForm', () => { ); }); - // @gate enableAsyncActions it('useActionState updates state asynchronously and queues multiple actions', async () => { let actionCounter = 0; async function action(state, type) { @@ -1052,7 +1036,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('2'); }); - // @gate enableAsyncActions it('useActionState supports inline actions', async () => { let increment; function App({stepSize}) { @@ -1084,7 +1067,6 @@ describe('ReactDOMForm', () => { assertLog(['Pending 1', '11']); }); - // @gate enableAsyncActions it('useActionState: dispatch throws if called during render', async () => { function App() { const [state, dispatch, isPending] = useActionState(async () => {}, 0); @@ -1100,7 +1082,6 @@ describe('ReactDOMForm', () => { }); }); - // @gate enableAsyncActions it('useActionState: queues multiple actions and runs them in order', async () => { let action; function App() { @@ -1132,7 +1113,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('D'); }); - // @gate enableAsyncActions it( 'useActionState: when calling a queued action, uses the implementation ' + 'that was current at the time it was dispatched, not the most recent one', @@ -1179,7 +1159,6 @@ describe('ReactDOMForm', () => { }, ); - // @gate enableAsyncActions it('useActionState: works if action is sync', async () => { let increment; function App({stepSize}) { @@ -1211,7 +1190,6 @@ describe('ReactDOMForm', () => { assertLog(['Pending 1', '11']); }); - // @gate enableAsyncActions it('useActionState: can mix sync and async actions', async () => { let action; function App() { @@ -1239,7 +1217,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('E'); }); - // @gate enableAsyncActions it('useActionState: error handling (sync action)', async () => { class ErrorBoundary extends React.Component { state = {error: null}; @@ -1288,7 +1265,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('Caught an error: Oops!'); }); - // @gate enableAsyncActions it('useActionState: error handling (async action)', async () => { class ErrorBoundary extends React.Component { state = {error: null}; @@ -1394,7 +1370,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('Caught an error: Oops!'); }); - // @gate enableAsyncActions it('useActionState works in StrictMode', async () => { let actionCounter = 0; async function action(state, type) { diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 98ec6f9236fed..1c673bf2543a0 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -16,7 +16,6 @@ import type { import {isValidContainer} from 'react-dom-bindings/src/client/ReactDOMContainer'; import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; -import {enableAsyncActions} from 'shared/ReactFeatureFlags'; export type RootType = { render(children: ReactNodeList): void, @@ -305,10 +304,8 @@ export function hydrateRoot( if (options.unstable_transitionCallbacks !== undefined) { transitionCallbacks = options.unstable_transitionCallbacks; } - if (enableAsyncActions) { - if (options.formState !== undefined) { - formState = options.formState; - } + if (options.formState !== undefined) { + formState = options.formState; } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index df222199b2012..119129d506c09 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -106,7 +106,6 @@ import { enableTransitionTracing, enableLegacyHidden, enableCPUSuspense, - enableAsyncActions, enablePostpone, enableRenderableContext, disableLegacyMode, @@ -1619,55 +1618,53 @@ function updateHostComponent( workInProgress.flags |= ContentReset; } - if (enableAsyncActions) { - const memoizedState = workInProgress.memoizedState; - if (memoizedState !== null) { - // This fiber has been upgraded to a stateful component. The only way - // happens currently is for form actions. We use hooks to track the - // pending and error state of the form. - // - // Once a fiber is upgraded to be stateful, it remains stateful for the - // rest of its lifetime. - const newState = renderTransitionAwareHostComponentWithHooks( - current, - workInProgress, - renderLanes, - ); + const memoizedState = workInProgress.memoizedState; + if (memoizedState !== null) { + // This fiber has been upgraded to a stateful component. The only way + // happens currently is for form actions. We use hooks to track the + // pending and error state of the form. + // + // Once a fiber is upgraded to be stateful, it remains stateful for the + // rest of its lifetime. + 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, - ); - } + // 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 514f93760981c..033e71610f5c6 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -44,7 +44,6 @@ import { enableUseEffectEventHook, enableLegacyCache, debugRenderPhaseSideEffectsForStrictMode, - enableAsyncActions, disableLegacyMode, enableNoCloningMemoCache, enableContextProfiling, @@ -907,9 +906,6 @@ export function renderTransitionAwareHostComponentWithHooks( workInProgress: Fiber, lanes: Lanes, ): TransitionStatus { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } return renderWithHooks( current, workInProgress, @@ -921,10 +917,6 @@ export function renderTransitionAwareHostComponentWithHooks( } export function TransitionAwareHostComponent(): TransitionStatus { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } - const dispatcher: any = ReactSharedInternals.H; const [maybeThenable] = dispatcher.useState(); let nextState; @@ -1490,7 +1482,7 @@ function updateReducerImpl( // Check if this is an optimistic update. const revertLane = update.revertLane; - if (!enableAsyncActions || revertLane === NoLane) { + if (revertLane === NoLane) { // This is not an optimistic update, and we're going to apply it now. // But, if there were earlier updates that were skipped, we need to // leave this update in the queue so it can be rebased later. @@ -3268,25 +3260,14 @@ function startTransition( const prevTransition = ReactSharedInternals.T; const currentTransition: BatchConfigTransition = {}; - if (enableAsyncActions) { - // We don't really need to use an optimistic update here, because we - // schedule a second "revert" update below (which we use to suspend the - // transition until the async action scope has finished). But we'll use an - // optimistic update anyway to make it less likely the behavior accidentally - // diverges; for example, both an optimistic update and this one should - // share the same lane. - ReactSharedInternals.T = currentTransition; - dispatchOptimisticSetState(fiber, false, queue, pendingState); - } else { - ReactSharedInternals.T = null; - dispatchSetStateInternal( - fiber, - queue, - pendingState, - requestUpdateLane(fiber), - ); - ReactSharedInternals.T = currentTransition; - } + // We don't really need to use an optimistic update here, because we + // schedule a second "revert" update below (which we use to suspend the + // transition until the async action scope has finished). But we'll use an + // optimistic update anyway to make it less likely the behavior accidentally + // diverges; for example, both an optimistic update and this one should + // share the same lane. + ReactSharedInternals.T = currentTransition; + dispatchOptimisticSetState(fiber, false, queue, pendingState); if (enableTransitionTracing) { if (options !== undefined && options.name !== undefined) { @@ -3300,78 +3281,61 @@ function startTransition( } try { - if (enableAsyncActions) { - const returnValue = callback(); - const onStartTransitionFinish = ReactSharedInternals.S; - if (onStartTransitionFinish !== null) { - onStartTransitionFinish(currentTransition, returnValue); - } + const returnValue = callback(); + const onStartTransitionFinish = ReactSharedInternals.S; + if (onStartTransitionFinish !== null) { + onStartTransitionFinish(currentTransition, returnValue); + } - // Check if we're inside an async action scope. If so, we'll entangle - // this new action with the existing scope. - // - // If we're not already inside an async action scope, and this action is - // async, then we'll create a new async scope. - // - // In the async case, the resulting render will suspend until the async - // action scope has finished. - if ( - returnValue !== null && - typeof returnValue === 'object' && - typeof returnValue.then === 'function' - ) { - const thenable = ((returnValue: any): Thenable); - // Create a thenable that resolves to `finishedState` once the async - // action has completed. - const thenableForFinishedState = chainThenableValue( - thenable, - finishedState, - ); - dispatchSetStateInternal( - fiber, - queue, - (thenableForFinishedState: any), - requestUpdateLane(fiber), - ); - } else { - dispatchSetStateInternal( - fiber, - queue, - finishedState, - requestUpdateLane(fiber), - ); - } - } else { - // Async actions are not enabled. + // Check if we're inside an async action scope. If so, we'll entangle + // this new action with the existing scope. + // + // If we're not already inside an async action scope, and this action is + // async, then we'll create a new async scope. + // + // In the async case, the resulting render will suspend until the async + // action scope has finished. + if ( + returnValue !== null && + typeof returnValue === 'object' && + typeof returnValue.then === 'function' + ) { + const thenable = ((returnValue: any): Thenable); + // Create a thenable that resolves to `finishedState` once the async + // action has completed. + const thenableForFinishedState = chainThenableValue( + thenable, + finishedState, + ); dispatchSetStateInternal( fiber, queue, - finishedState, + (thenableForFinishedState: any), requestUpdateLane(fiber), ); - callback(); - } - } catch (error) { - if (enableAsyncActions) { - // This is a trick to get the `useTransition` hook to rethrow the error. - // When it unwraps the thenable with the `use` algorithm, the error - // will be thrown. - const rejectedThenable: RejectedThenable = { - then() {}, - status: 'rejected', - reason: error, - }; + } else { dispatchSetStateInternal( fiber, queue, - rejectedThenable, + finishedState, requestUpdateLane(fiber), ); - } else { - // The error rethrowing behavior is only enabled when the async actions - // feature is on, even for sync actions. - throw error; } + } catch (error) { + // This is a trick to get the `useTransition` hook to rethrow the error. + // When it unwraps the thenable with the `use` algorithm, the error + // will be thrown. + const rejectedThenable: RejectedThenable = { + then() {}, + status: 'rejected', + reason: error, + }; + dispatchSetStateInternal( + fiber, + queue, + rejectedThenable, + requestUpdateLane(fiber), + ); } finally { setCurrentUpdatePriority(previousPriority); @@ -3401,15 +3365,6 @@ export function startHostTransition( action: (F => mixed) | null, formData: F, ): void { - if (!enableAsyncActions) { - // Form actions are enabled, but async actions are not. Call the function, - // but don't handle any pending or error states. - if (action !== null) { - action(formData); - } - return; - } - if (formFiber.tag !== HostComponent) { throw new Error( 'Expected the form instance to be a HostComponent. This ' + @@ -3595,9 +3550,6 @@ function rerenderTransition(): [ } function useHostTransitionStatus(): TransitionStatus { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } return readContext(HostTransitionContext); } @@ -4026,6 +3978,10 @@ export const ContextOnlyDispatcher: Dispatcher = { useTransition: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useId: throwInvalidHookError, + useHostTransitionStatus: throwInvalidHookError, + useFormState: throwInvalidHookError, + useActionState: throwInvalidHookError, + useOptimistic: throwInvalidHookError, }; if (enableCache) { (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError; @@ -4039,15 +3995,6 @@ if (enableUseEffectEventHook) { if (enableUseResourceEffectHook) { (ContextOnlyDispatcher: Dispatcher).useResourceEffect = throwInvalidHookError; } -if (enableAsyncActions) { - (ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus = - throwInvalidHookError; - (ContextOnlyDispatcher: Dispatcher).useFormState = throwInvalidHookError; - (ContextOnlyDispatcher: Dispatcher).useActionState = throwInvalidHookError; -} -if (enableAsyncActions) { - (ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError; -} if (enableContextProfiling) { (ContextOnlyDispatcher: Dispatcher).unstable_useContextWithBailout = throwInvalidHookError; @@ -4072,6 +4019,10 @@ const HooksDispatcherOnMount: Dispatcher = { useTransition: mountTransition, useSyncExternalStore: mountSyncExternalStore, useId: mountId, + useHostTransitionStatus: useHostTransitionStatus, + useFormState: mountActionState, + useActionState: mountActionState, + useOptimistic: mountOptimistic, }; if (enableCache) { (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh; @@ -4085,15 +4036,6 @@ if (enableUseEffectEventHook) { if (enableUseResourceEffectHook) { (HooksDispatcherOnMount: Dispatcher).useResourceEffect = mountResourceEffect; } -if (enableAsyncActions) { - (HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnMount: Dispatcher).useFormState = mountActionState; - (HooksDispatcherOnMount: Dispatcher).useActionState = mountActionState; -} -if (enableAsyncActions) { - (HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic; -} if (enableContextProfiling) { (HooksDispatcherOnMount: Dispatcher).unstable_useContextWithBailout = unstable_useContextWithBailout; @@ -4118,6 +4060,10 @@ const HooksDispatcherOnUpdate: Dispatcher = { useTransition: updateTransition, useSyncExternalStore: updateSyncExternalStore, useId: updateId, + useHostTransitionStatus: useHostTransitionStatus, + useFormState: updateActionState, + useActionState: updateActionState, + useOptimistic: updateOptimistic, }; if (enableCache) { (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh; @@ -4132,15 +4078,6 @@ if (enableUseResourceEffectHook) { (HooksDispatcherOnUpdate: Dispatcher).useResourceEffect = updateResourceEffect; } -if (enableAsyncActions) { - (HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnUpdate: Dispatcher).useFormState = updateActionState; - (HooksDispatcherOnUpdate: Dispatcher).useActionState = updateActionState; -} -if (enableAsyncActions) { - (HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic; -} if (enableContextProfiling) { (HooksDispatcherOnUpdate: Dispatcher).unstable_useContextWithBailout = unstable_useContextWithBailout; @@ -4165,6 +4102,10 @@ const HooksDispatcherOnRerender: Dispatcher = { useTransition: rerenderTransition, useSyncExternalStore: updateSyncExternalStore, useId: updateId, + useHostTransitionStatus: useHostTransitionStatus, + useFormState: rerenderActionState, + useActionState: rerenderActionState, + useOptimistic: rerenderOptimistic, }; if (enableCache) { (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh; @@ -4179,15 +4120,6 @@ if (enableUseResourceEffectHook) { (HooksDispatcherOnRerender: Dispatcher).useResourceEffect = updateResourceEffect; } -if (enableAsyncActions) { - (HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnRerender: Dispatcher).useFormState = rerenderActionState; - (HooksDispatcherOnRerender: Dispatcher).useActionState = rerenderActionState; -} -if (enableAsyncActions) { - (HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic; -} if (enableContextProfiling) { (HooksDispatcherOnRerender: Dispatcher).unstable_useContextWithBailout = unstable_useContextWithBailout; @@ -4347,6 +4279,34 @@ if (__DEV__) { mountHookTypesDev(); return mountId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + mountHookTypesDev(); + warnOnUseFormStateInDev(); + return mountActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + mountHookTypesDev(); + return mountActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + mountHookTypesDev(); + return mountOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = @@ -4390,42 +4350,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnMountInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - mountHookTypesDev(); - warnOnUseFormStateInDev(); - return mountActionState(action, initialState, permalink); - }; - (HooksDispatcherOnMountInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - mountHookTypesDev(); - return mountActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (HooksDispatcherOnMountInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - mountHookTypesDev(); - return mountOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -4559,6 +4483,34 @@ if (__DEV__) { updateHookTypesDev(); return mountId(); }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + updateHookTypesDev(); + return mountActionState(action, initialState, permalink); + }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + updateHookTypesDev(); + warnOnUseFormStateInDev(); + return mountActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + updateHookTypesDev(); + return mountOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = @@ -4602,42 +4554,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - updateHookTypesDev(); - warnOnUseFormStateInDev(); - return mountActionState(action, initialState, permalink); - }; - (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - updateHookTypesDev(); - return mountActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - updateHookTypesDev(); - return mountOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -4771,6 +4687,34 @@ if (__DEV__) { updateHookTypesDev(); return updateId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + updateHookTypesDev(); + warnOnUseFormStateInDev(); + return updateActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + updateHookTypesDev(); + return updateActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + updateHookTypesDev(); + return updateOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = @@ -4813,42 +4757,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - updateHookTypesDev(); - warnOnUseFormStateInDev(); - return updateActionState(action, initialState, permalink); - }; - (HooksDispatcherOnUpdateInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - updateHookTypesDev(); - return updateActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (HooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - updateHookTypesDev(); - return updateOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -4982,6 +4890,34 @@ if (__DEV__) { updateHookTypesDev(); return updateId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + updateHookTypesDev(); + warnOnUseFormStateInDev(); + return rerenderActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + updateHookTypesDev(); + return rerenderActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + updateHookTypesDev(); + return rerenderOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = @@ -5024,42 +4960,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - updateHookTypesDev(); - warnOnUseFormStateInDev(); - return rerenderActionState(action, initialState, permalink); - }; - (HooksDispatcherOnRerenderInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - updateHookTypesDev(); - return rerenderActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (HooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - updateHookTypesDev(); - return rerenderOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (HooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -5212,6 +5112,36 @@ if (__DEV__) { mountHookTypesDev(); return mountId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = @@ -5260,44 +5190,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountActionState(action, initialState, permalink); - }; - (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -5451,6 +5343,36 @@ if (__DEV__) { updateHookTypesDev(); return updateId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = @@ -5499,44 +5421,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateActionState(action, initialState, permalink); - }; - (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( @@ -5690,6 +5574,36 @@ if (__DEV__) { updateHookTypesDev(); return updateId(); }, + useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useFormState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderActionState(action, initialState, permalink); + }, + useActionState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, + ): [Awaited, (P) => void, boolean] { + currentHookNameInDev = 'useActionState'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderActionState(action, initialState, permalink); + }, + useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, + ): [S, (A) => void] { + currentHookNameInDev = 'useOptimistic'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return rerenderOptimistic(passthrough, reducer); + }, + useHostTransitionStatus, }; if (enableCache) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = @@ -5738,44 +5652,6 @@ if (__DEV__) { ); }; } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus = - useHostTransitionStatus; - (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState = - function useFormState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useFormState'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return rerenderActionState(action, initialState, permalink); - }; - (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useActionState = - function useActionState( - action: (Awaited, P) => S, - initialState: Awaited, - permalink?: string, - ): [Awaited, (P) => void, boolean] { - currentHookNameInDev = 'useActionState'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return rerenderActionState(action, initialState, permalink); - }; - } - if (enableAsyncActions) { - (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic = - function useOptimistic( - passthrough: S, - reducer: ?(S, A) => S, - ): [S, (A) => void] { - currentHookNameInDev = 'useOptimistic'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return rerenderOptimistic(passthrough, reducer); - }; - } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = function ( diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index a3b525b1a0bf0..10ea377fc2b39 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -20,7 +20,6 @@ import { isPrimaryRenderer, } from './ReactFiberConfig'; import {createCursor, push, pop} from './ReactFiberStack'; -import {enableAsyncActions} from 'shared/ReactFeatureFlags'; const contextStackCursor: StackCursor = createCursor(null); const contextFiberStackCursor: StackCursor = createCursor(null); @@ -91,13 +90,11 @@ function getHostContext(): HostContext { } function pushHostContext(fiber: Fiber): void { - if (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 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); @@ -120,25 +117,23 @@ function popHostContext(fiber: Fiber): void { pop(contextFiberStackCursor, fiber); } - if (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 `NotPendingTransition`. We can do this because you're not allowed 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 = NotPendingTransition; - } else { - HostTransitionContext._currentValue2 = NotPendingTransition; - } + 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 `NotPendingTransition`. We can do this because you're not allowed 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 = NotPendingTransition; + } else { + HostTransitionContext._currentValue2 = NotPendingTransition; } } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 2e8d01e99e89c..190475519e1ca 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -45,7 +45,6 @@ import {createUpdate, ForceUpdate} from './ReactFiberClassUpdateQueue'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; import { enableLazyContextPropagation, - enableAsyncActions, enableRenderableContext, } from 'shared/ReactFeatureFlags'; import {getHostTransitionProvider} from './ReactFiberHostContext'; @@ -598,7 +597,7 @@ function propagateParentContextChanges( } } } - } else if (enableAsyncActions && parent === getHostTransitionProvider()) { + } else if (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; diff --git a/packages/react-reconciler/src/ReactFiberTransition.js b/packages/react-reconciler/src/ReactFiberTransition.js index 8ddd7083363a8..8953ca1457271 100644 --- a/packages/react-reconciler/src/ReactFiberTransition.js +++ b/packages/react-reconciler/src/ReactFiberTransition.js @@ -16,11 +16,7 @@ import type { Transition, } from './ReactFiberTracingMarkerComponent'; -import { - enableCache, - enableTransitionTracing, - enableAsyncActions, -} from 'shared/ReactFeatureFlags'; +import {enableCache, enableTransitionTracing} from 'shared/ReactFeatureFlags'; import {isPrimaryRenderer} from './ReactFiberConfig'; import {createCursor, push, pop} from './ReactFiberStack'; import { @@ -65,7 +61,6 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler( returnValue: mixed, ) { if ( - enableAsyncActions && typeof returnValue === 'object' && returnValue !== null && typeof returnValue.then === 'function' diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index dc6ab646b5445..2ef3a2da4f535 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -448,17 +448,17 @@ export type Dispatcher = { useId(): string, useCacheRefresh?: () => (?() => T, ?T) => void, useMemoCache?: (size: number) => Array, - useHostTransitionStatus?: () => TransitionStatus, - useOptimistic?: ( + useHostTransitionStatus: () => TransitionStatus, + useOptimistic: ( passthrough: S, reducer: ?(S, A) => S, ) => [S, (A) => void], - useFormState?: ( + useFormState: ( action: (Awaited, P) => S, initialState: Awaited, permalink?: string, ) => [Awaited, (P) => void, boolean], - useActionState?: ( + useActionState: ( action: (Awaited, P) => S, initialState: Awaited, permalink?: string, diff --git a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js index 2a65660b20f4b..843d5b2dd3fe6 100644 --- a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js +++ b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js @@ -121,7 +121,6 @@ describe('ReactAsyncActions', () => { return text; } - // @gate enableAsyncActions it('isPending remains true until async action finishes', async () => { let startTransition; function App() { @@ -154,7 +153,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput('Pending: false'); }); - // @gate enableAsyncActions it('multiple updates in an async action scope are entangled together', async () => { let startTransition; function App({text}) { @@ -210,7 +208,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it('multiple async action updates in the same scope are entangled together', async () => { let setStepA; function A() { @@ -350,7 +347,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it('urgent updates are not blocked during an async action', async () => { let setStepA; function A() { @@ -431,7 +427,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it("if a sync action throws, it's rethrown from the `useTransition`", async () => { class ErrorBoundary extends React.Component { state = {error: null}; @@ -473,7 +468,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput('Oops!'); }); - // @gate enableAsyncActions it("if an async action throws, it's rethrown from the `useTransition`", async () => { class ErrorBoundary extends React.Component { state = {error: null}; @@ -521,34 +515,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput('Oops!'); }); - // @gate !enableAsyncActions - it('when enableAsyncActions is disabled, and a sync action throws, `isPending` is turned off', async () => { - let startTransition; - function App() { - const [isPending, _start] = useTransition(); - startTransition = _start; - return ; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog(['Pending: false']); - expect(root).toMatchRenderedOutput('Pending: false'); - - await act(() => { - expect(() => { - startTransition(() => { - throw new Error('Oops!'); - }); - }).toThrow('Oops!'); - }); - assertLog(['Pending: true', 'Pending: false']); - expect(root).toMatchRenderedOutput('Pending: false'); - }); - - // @gate enableAsyncActions it('if there are multiple entangled actions, and one of them errors, it only affects that action', async () => { class ErrorBoundary extends React.Component { state = {error: null}; @@ -665,7 +631,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it('useOptimistic can be used to implement a pending state', async () => { const startTransition = React.startTransition; @@ -715,7 +680,6 @@ describe('ReactAsyncActions', () => { ]); }); - // @gate enableAsyncActions it('useOptimistic rebases pending updates on top of passthrough value', async () => { let serverCart = ['A']; @@ -836,7 +800,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it( 'regression: when there are no pending transitions, useOptimistic should ' + 'always return the passthrough value', @@ -882,7 +845,6 @@ describe('ReactAsyncActions', () => { }, ); - // @gate enableAsyncActions it('regression: useOptimistic during setState-in-render', async () => { // This is a regression test for a very specific case where useOptimistic is // the first hook in the component, it has a pending update, and a later @@ -920,7 +882,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput('1'); }); - // @gate enableAsyncActions it('useOptimistic accepts a custom reducer', async () => { let serverCart = ['A']; @@ -1052,7 +1013,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it('useOptimistic rebases if the passthrough is updated during a render phase update', async () => { // This is kind of an esoteric case where it's hard to come up with a // realistic real-world scenario but it should still work. @@ -1137,7 +1097,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput(
Count: 3
); }); - // @gate enableAsyncActions it('useOptimistic rebases if the passthrough is updated during a render phase update (initial mount)', async () => { // This is kind of an esoteric case where it's hard to come up with a // realistic real-world scenario but it should still work. @@ -1177,7 +1136,6 @@ describe('ReactAsyncActions', () => { ); }); - // @gate enableAsyncActions it('useOptimistic can update repeatedly in the same async action', async () => { let startTransition; let setLoadingProgress; @@ -1241,7 +1199,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput(
B
); }); - // @gate enableAsyncActions it('useOptimistic warns if outside of a transition', async () => { let startTransition; let setLoadingProgress; @@ -1289,7 +1246,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput(
B
); }); - // @gate enableAsyncActions it( 'optimistic state is not reverted until async action finishes, even if ' + 'useTransition hook is unmounted', @@ -1392,7 +1348,6 @@ describe('ReactAsyncActions', () => { }, ); - // @gate enableAsyncActions it( 'updates in an async action are entangled even if useTransition hook ' + 'is unmounted before it finishes', @@ -1480,7 +1435,6 @@ describe('ReactAsyncActions', () => { }, ); - // @gate enableAsyncActions it( 'updates in an async action are entangled even if useTransition hook ' + 'is unmounted before it finishes (class component)', @@ -1575,7 +1529,6 @@ describe('ReactAsyncActions', () => { }, ); - // @gate enableAsyncActions it( 'updates in an async action are entangled even if useTransition hook ' + 'is unmounted before it finishes (root update)', @@ -1660,7 +1613,6 @@ describe('ReactAsyncActions', () => { }, ); - // @gate enableAsyncActions it('React.startTransition supports async actions', async () => { const startTransition = React.startTransition; @@ -1698,7 +1650,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput('C'); }); - // @gate enableAsyncActions it('useOptimistic works with async actions passed to React.startTransition', async () => { const startTransition = React.startTransition; @@ -1745,7 +1696,6 @@ describe('ReactAsyncActions', () => { expect(root).toMatchRenderedOutput(Updated); }); - // @gate enableAsyncActions it( 'regression: updates in an action passed to React.startTransition are batched ' + 'even if there were no updates before the first await', @@ -1816,19 +1766,14 @@ describe('ReactAsyncActions', () => { ); it('React.startTransition captures async errors and passes them to reportError', async () => { - // NOTE: This is gated here instead of using the pragma because the failure - // happens asynchronously and the `gate` runtime doesn't capture it. - if (gate(flags => flags.enableAsyncActions)) { - await act(() => { - React.startTransition(async () => { - throw new Error('Oops'); - }); + await act(() => { + React.startTransition(async () => { + throw new Error('Oops'); }); - assertLog(['reportError: Oops']); - } + }); + assertLog(['reportError: Oops']); }); - // @gate enableAsyncActions it('React.startTransition captures sync errors and passes them to reportError', async () => { await act(() => { try { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index b33e2a38ed305..f98b79dd8a890 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -361,7 +361,6 @@ describe('ReactFlightDOMForm', () => { expect(foo).toBe('barobject'); }); - // @gate enableAsyncActions it("useActionState's dispatch binds the initial state to the provided action", async () => { const serverAction = serverExports( async function action(prevState, formData) { @@ -409,7 +408,6 @@ describe('ReactFlightDOMForm', () => { expect(await returnValue).toEqual({count: 6}); }); - // @gate enableAsyncActions it('useActionState can reuse state during MPA form submission', async () => { const serverAction = serverExports( async function action(prevState, formData) { @@ -498,7 +496,6 @@ describe('ReactFlightDOMForm', () => { } }); - // @gate enableAsyncActions it( 'useActionState preserves state if arity is the same, but different ' + 'arguments are bound (i.e. inline closure)', @@ -617,7 +614,6 @@ describe('ReactFlightDOMForm', () => { }, ); - // @gate enableAsyncActions it('useActionState does not reuse state if action signatures are different', async () => { // This is the same as the previous test, except instead of using bind to // configure the server action (i.e. a closure), it swaps the action. @@ -704,7 +700,6 @@ describe('ReactFlightDOMForm', () => { expect(container.textContent).toBe('111'); }); - // @gate enableAsyncActions it('when permalink is provided, useActionState compares that instead of the keypath', async () => { const serverAction = serverExports( async function action(prevState, formData) { @@ -810,7 +805,6 @@ describe('ReactFlightDOMForm', () => { expect(container.textContent).toBe('1'); }); - // @gate enableAsyncActions it('useActionState can change the action URL with the `permalink` argument', async () => { const serverAction = serverExports(function action(prevState) { return {state: prevState.count + 1}; @@ -855,7 +849,6 @@ describe('ReactFlightDOMForm', () => { expect(form.action).toBe('http://localhost/permalink'); }); - // @gate enableAsyncActions it('useActionState `permalink` is coerced to string', async () => { const serverAction = serverExports(function action(prevState) { return {state: prevState.count + 1}; @@ -908,7 +901,6 @@ describe('ReactFlightDOMForm', () => { expect(form.action).toBe('http://localhost/permalink'); }); - // @gate enableAsyncActions it('useActionState can return JSX state during MPA form submission', async () => { const serverAction = serverExports( async function action(prevState, formData) { @@ -980,7 +972,7 @@ describe('ReactFlightDOMForm', () => { expect(form2.firstChild.tagName).toBe('DIV'); }); - // @gate enableAsyncActions && enableBinaryFlight + // @gate enableBinaryFlight it('useActionState can return binary state during MPA form submission', async () => { const serverAction = serverExports( async function action(prevState, formData) { diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 84b9448ca32f6..e4e81af8fabb8 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -42,7 +42,6 @@ import { enableCache, enableUseEffectEventHook, enableUseMemoCacheHook, - enableAsyncActions, enableUseResourceEffectHook, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; @@ -833,6 +832,10 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useId, // Subscriptions are not setup in a server environment. useSyncExternalStore, + useOptimistic, + useActionState, + useFormState: useActionState, + useHostTransitionStatus, } : { readContext, @@ -852,6 +855,10 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useTransition: clientHookNotSupported, useId, useSyncExternalStore: clientHookNotSupported, + useOptimistic, + useActionState, + useFormState: useActionState, + useHostTransitionStatus, }; if (enableCache) { @@ -863,14 +870,6 @@ if (enableUseEffectEventHook) { if (enableUseMemoCacheHook) { HooksDispatcher.useMemoCache = useMemoCache; } -if (enableAsyncActions) { - HooksDispatcher.useHostTransitionStatus = useHostTransitionStatus; -} -if (enableAsyncActions) { - HooksDispatcher.useOptimistic = useOptimistic; - HooksDispatcher.useFormState = useActionState; - HooksDispatcher.useActionState = useActionState; -} if (enableUseResourceEffectHook) { HooksDispatcher.useResourceEffect = supportsClientAPIs ? noop diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index e85e0e9ce6a60..d0351e38c86aa 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -77,6 +77,10 @@ export const HooksDispatcher: Dispatcher = { useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), useId, + useHostTransitionStatus: (unsupportedHook: any), + useOptimistic: (unsupportedHook: any), + useFormState: (unsupportedHook: any), + useActionState: (unsupportedHook: any), useSyncExternalStore: (unsupportedHook: any), useCacheRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 90b904bb42c75..40c8a23d3798b 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -18,10 +18,7 @@ import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import { - enableAsyncActions, - enableUseResourceEffectHook, -} from 'shared/ReactFeatureFlags'; +import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags'; import { enableContextProfiling, enableLazyContextPropagation, @@ -255,7 +252,6 @@ export function useOptimistic( reducer: ?(S, A) => S, ): [S, (A) => void] { const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.useOptimistic(passthrough, reducer); } @@ -264,11 +260,6 @@ export function useActionState( initialState: Awaited, permalink?: string, ): [Awaited, (P) => void, boolean] { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } else { - const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional - return dispatcher.useActionState(action, initialState, permalink); - } + const dispatcher = resolveDispatcher(); + return dispatcher.useActionState(action, initialState, permalink); } diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index a9b30c424a07f..99ccbb959b015 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -11,10 +11,7 @@ import type {StartTransitionOptions} from 'shared/ReactTypes'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import { - enableAsyncActions, - enableTransitionTracing, -} from 'shared/ReactFeatureFlags'; +import {enableTransitionTracing} from 'shared/ReactFeatureFlags'; import reportGlobalError from 'shared/reportGlobalError'; @@ -37,35 +34,24 @@ export function startTransition( } } - if (enableAsyncActions) { - try { - const returnValue = scope(); - const onStartTransitionFinish = ReactSharedInternals.S; - if (onStartTransitionFinish !== null) { - onStartTransitionFinish(currentTransition, returnValue); - } - if ( - typeof returnValue === 'object' && - returnValue !== null && - typeof returnValue.then === 'function' - ) { - returnValue.then(noop, reportGlobalError); - } - } catch (error) { - reportGlobalError(error); - } finally { - warnAboutTransitionSubscriptions(prevTransition, currentTransition); - ReactSharedInternals.T = prevTransition; + try { + const returnValue = scope(); + const onStartTransitionFinish = ReactSharedInternals.S; + if (onStartTransitionFinish !== null) { + onStartTransitionFinish(currentTransition, returnValue); } - } else { - // When async actions are not enabled, startTransition does not - // capture errors. - try { - scope(); - } finally { - warnAboutTransitionSubscriptions(prevTransition, currentTransition); - ReactSharedInternals.T = prevTransition; + if ( + typeof returnValue === 'object' && + returnValue !== null && + typeof returnValue.then === 'function' + ) { + returnValue.then(noop, reportGlobalError); } + } catch (error) { + reportGlobalError(error); + } finally { + warnAboutTransitionSubscriptions(prevTransition, currentTransition); + ReactSharedInternals.T = prevTransition; } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index edaf4b2419136..a96d0e1917434 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -31,7 +31,6 @@ export const enableComponentStackLocations = true; // TODO: Finish rolling out in www export const favorSafetyOverHydrationPerf = true; -export const enableAsyncActions = true; // Need to remove didTimeout argument from Scheduler before landing export const disableSchedulerTimeoutInWorkLoop = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 1ecbcfe28f5ce..eb3f5f3cd5553 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -42,7 +42,6 @@ export const disableLegacyContextForFunctionComponents = false; export const disableLegacyMode = false; export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; -export const enableAsyncActions = true; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableBinaryFlight = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 0d97b0b68d263..d725ecba679ce 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -29,7 +29,6 @@ export const disableLegacyContextForFunctionComponents = true; export const disableLegacyMode = false; export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; -export const enableAsyncActions = true; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableBinaryFlight = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 28f96fb84e6bc..2c6892de26d96 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -67,8 +67,6 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFizzExternalRuntime = true; export const enableDeferRootSchedulingToMicrotask = true; -export const enableAsyncActions = true; - export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index fe3e2bd4813b8..eff87c5683fc8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -21,7 +21,6 @@ export const disableLegacyContextForFunctionComponents = false; export const disableLegacyMode = false; export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; -export const enableAsyncActions = true; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableBinaryFlight = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 1badea3abf309..00a0463353dda 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -69,8 +69,6 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFizzExternalRuntime = false; export const enableDeferRootSchedulingToMicrotask = true; -export const enableAsyncActions = true; - export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 2a432305635d3..65edbea8e153a 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -58,7 +58,6 @@ export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = true; export const enableFilterEmptyStringAttributesDOM = true; export const enableMoveBefore = false; -export const enableAsyncActions = true; export const disableInputAttributeSyncing = false; export const enableLegacyFBSupport = true; export const enableLazyContextPropagation = true; From 08dfd0b8053d8d712607d3f37ddbb4da00c351f1 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 13 Dec 2024 14:50:13 -0500 Subject: [PATCH 2/6] Remove enableBinaryflight (#31759) Based off https://github.com/facebook/react/pull/31757 This has landed everywhere. --- .../react-client/src/ReactFlightClient.js | 154 ++++---- .../src/ReactFlightReplyClient.js | 129 ++++--- .../src/__tests__/ReactFlight-test.js | 2 +- .../src/__tests__/ReactFlightDOMEdge-test.js | 7 +- .../src/__tests__/ReactFlightDOMForm-test.js | 1 - .../src/__tests__/ReactFlightDOMNode-test.js | 1 - .../__tests__/ReactFlightDOMReplyEdge-test.js | 7 +- .../src/ReactFlightReplyServer.js | 86 ++--- .../react-server/src/ReactFlightServer.js | 351 +++++++++--------- packages/react/src/ReactTaint.js | 6 +- packages/shared/ReactFeatureFlags.js | 1 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 17 files changed, 352 insertions(+), 399 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 4d5204ab00220..259864e91d398 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -43,7 +43,6 @@ import type {Postpone} from 'react/src/ReactPostpone'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; import { - enableBinaryFlight, enablePostpone, enableFlightReadableStream, enableOwnerStacks, @@ -1461,11 +1460,8 @@ function parseModelString( } case 'B': { // Blob - if (enableBinaryFlight) { - const ref = value.slice(2); - return getOutlinedModel(response, ref, parentObject, key, createBlob); - } - return undefined; + const ref = value.slice(2); + return getOutlinedModel(response, ref, parentObject, key, createBlob); } case 'K': { // FormData @@ -2821,53 +2817,51 @@ function processFullBinaryRow( buffer: Array, chunk: Uint8Array, ): void { - if (enableBinaryFlight) { - switch (tag) { - case 65 /* "A" */: - // We must always clone to extract it into a separate buffer instead of just a view. - resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); - return; - case 79 /* "O" */: - resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); - return; - case 111 /* "o" */: - resolveBuffer( - response, - id, - buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), - ); - return; - case 85 /* "U" */: - resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); - return; - case 83 /* "S" */: - resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); - return; - case 115 /* "s" */: - resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); - return; - case 76 /* "L" */: - resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); - return; - case 108 /* "l" */: - resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); - return; - case 71 /* "G" */: - resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); - return; - case 103 /* "g" */: - resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); - return; - case 77 /* "M" */: - resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); - return; - case 109 /* "m" */: - resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); - return; - case 86 /* "V" */: - resolveTypedArray(response, id, buffer, chunk, DataView, 1); - return; - } + switch (tag) { + case 65 /* "A" */: + // We must always clone to extract it into a separate buffer instead of just a view. + resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); + return; + case 79 /* "O" */: + resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); + return; + case 111 /* "o" */: + resolveBuffer( + response, + id, + buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), + ); + return; + case 85 /* "U" */: + resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); + return; + case 83 /* "S" */: + resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); + return; + case 115 /* "s" */: + resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); + return; + case 76 /* "L" */: + resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); + return; + case 108 /* "l" */: + resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); + return; + case 71 /* "G" */: + resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); + return; + case 103 /* "g" */: + resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); + return; + case 77 /* "M" */: + resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); + return; + case 109 /* "m" */: + resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); + return; + case 86 /* "V" */: + resolveTypedArray(response, id, buffer, chunk, DataView, 1); + return; } const stringDecoder = response._stringDecoder; @@ -3061,20 +3055,19 @@ export function processBinaryChunk( const resolvedRowTag = chunk[i]; if ( resolvedRowTag === 84 /* "T" */ || - (enableBinaryFlight && - (resolvedRowTag === 65 /* "A" */ || - resolvedRowTag === 79 /* "O" */ || - resolvedRowTag === 111 /* "o" */ || - resolvedRowTag === 85 /* "U" */ || - resolvedRowTag === 83 /* "S" */ || - resolvedRowTag === 115 /* "s" */ || - resolvedRowTag === 76 /* "L" */ || - resolvedRowTag === 108 /* "l" */ || - resolvedRowTag === 71 /* "G" */ || - resolvedRowTag === 103 /* "g" */ || - resolvedRowTag === 77 /* "M" */ || - resolvedRowTag === 109 /* "m" */ || - resolvedRowTag === 86)) /* "V" */ + resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86 /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; @@ -3187,20 +3180,19 @@ export function processStringChunk(response: Response, chunk: string): void { const resolvedRowTag = chunk.charCodeAt(i); if ( resolvedRowTag === 84 /* "T" */ || - (enableBinaryFlight && - (resolvedRowTag === 65 /* "A" */ || - resolvedRowTag === 79 /* "O" */ || - resolvedRowTag === 111 /* "o" */ || - resolvedRowTag === 85 /* "U" */ || - resolvedRowTag === 83 /* "S" */ || - resolvedRowTag === 115 /* "s" */ || - resolvedRowTag === 76 /* "L" */ || - resolvedRowTag === 108 /* "l" */ || - resolvedRowTag === 71 /* "G" */ || - resolvedRowTag === 103 /* "g" */ || - resolvedRowTag === 77 /* "M" */ || - resolvedRowTag === 109 /* "m" */ || - resolvedRowTag === 86)) /* "V" */ + resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86 /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index bfb2573ea35b0..81212437a09a0 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -20,7 +20,6 @@ import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; import { enableRenderableContext, - enableBinaryFlight, enableFlightReadableStream, } from 'shared/ReactFeatureFlags'; @@ -578,73 +577,71 @@ export function processReply( return serializeSetID(setId); } - if (enableBinaryFlight) { - if (value instanceof ArrayBuffer) { - const blob = new Blob([value]); - const blobId = nextPartId++; - if (formData === null) { - formData = new FormData(); - } - formData.append(formFieldPrefix + blobId, blob); - return '$' + 'A' + blobId.toString(16); - } - if (value instanceof Int8Array) { - // char - return serializeTypedArray('O', value); - } - if (value instanceof Uint8Array) { - // unsigned char - return serializeTypedArray('o', value); - } - if (value instanceof Uint8ClampedArray) { - // unsigned clamped char - return serializeTypedArray('U', value); - } - if (value instanceof Int16Array) { - // sort - return serializeTypedArray('S', value); - } - if (value instanceof Uint16Array) { - // unsigned short - return serializeTypedArray('s', value); - } - if (value instanceof Int32Array) { - // long - return serializeTypedArray('L', value); - } - if (value instanceof Uint32Array) { - // unsigned long - return serializeTypedArray('l', value); - } - if (value instanceof Float32Array) { - // float - return serializeTypedArray('G', value); - } - if (value instanceof Float64Array) { - // double - return serializeTypedArray('g', value); - } - if (value instanceof BigInt64Array) { - // number - return serializeTypedArray('M', value); - } - if (value instanceof BigUint64Array) { - // unsigned number - // We use "m" instead of "n" since JSON can start with "null" - return serializeTypedArray('m', value); - } - if (value instanceof DataView) { - return serializeTypedArray('V', value); + if (value instanceof ArrayBuffer) { + const blob = new Blob([value]); + const blobId = nextPartId++; + if (formData === null) { + formData = new FormData(); } - // TODO: Blob is not available in old Node/browsers. Remove the typeof check later. - if (typeof Blob === 'function' && value instanceof Blob) { - if (formData === null) { - formData = new FormData(); - } - const blobId = nextPartId++; - formData.append(formFieldPrefix + blobId, value); - return serializeBlobID(blobId); + formData.append(formFieldPrefix + blobId, blob); + return '$' + 'A' + blobId.toString(16); + } + if (value instanceof Int8Array) { + // char + return serializeTypedArray('O', value); + } + if (value instanceof Uint8Array) { + // unsigned char + return serializeTypedArray('o', value); + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + return serializeTypedArray('U', value); + } + if (value instanceof Int16Array) { + // sort + return serializeTypedArray('S', value); + } + if (value instanceof Uint16Array) { + // unsigned short + return serializeTypedArray('s', value); + } + if (value instanceof Int32Array) { + // long + return serializeTypedArray('L', value); + } + if (value instanceof Uint32Array) { + // unsigned long + return serializeTypedArray('l', value); + } + if (value instanceof Float32Array) { + // float + return serializeTypedArray('G', value); + } + if (value instanceof Float64Array) { + // double + return serializeTypedArray('g', value); + } + if (value instanceof BigInt64Array) { + // number + return serializeTypedArray('M', value); + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + return serializeTypedArray('m', value); + } + if (value instanceof DataView) { + return serializeTypedArray('V', value); + } + // TODO: Blob is not available in old Node/browsers. Remove the typeof check later. + if (typeof Blob === 'function' && value instanceof Blob) { + if (formData === null) { + formData = new FormData(); } + const blobId = nextPartId++; + formData.append(formFieldPrefix + blobId, value); + return serializeBlobID(blobId); } const iteratorFn = getIteratorFn(value); diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 75220b762918e..af12c583db11a 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2059,7 +2059,7 @@ describe('ReactFlight', () => { expect(errors).toEqual(['Cannot pass a secret token to the client']); }); - // @gate enableTaint && enableBinaryFlight + // @gate enableTaint it('errors when a tainted binary value is serialized', async () => { function UserClient({user}) { return {user.name}; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index e702ae8f8e84f..3bd481205f1b4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -524,7 +524,6 @@ describe('ReactFlightDOMEdge', () => { expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize); }); - // @gate enableBinaryFlight it('should be able to serialize any kind of typed array', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -556,7 +555,6 @@ describe('ReactFlightDOMEdge', () => { expect(result).toEqual(buffers); }); - // @gate enableBinaryFlight it('should be able to serialize a blob', async () => { const bytes = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -578,7 +576,6 @@ describe('ReactFlightDOMEdge', () => { expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer()); }); - // @gate enableBinaryFlight it('can transport FormData (blobs)', async () => { const bytes = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -823,7 +820,7 @@ describe('ReactFlightDOMEdge', () => { ); }); - // @gate enableFlightReadableStream && enableBinaryFlight + // @gate enableFlightReadableStream it('should supports ReadableStreams with typed arrays', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -882,7 +879,7 @@ describe('ReactFlightDOMEdge', () => { expect(streamedBuffers).toEqual(buffers); }); - // @gate enableFlightReadableStream && enableBinaryFlight + // @gate enableFlightReadableStream it('should support BYOB binary ReadableStreams', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index f98b79dd8a890..0b4549d5bac47 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -972,7 +972,6 @@ describe('ReactFlightDOMForm', () => { expect(form2.firstChild.tagName).toBe('DIV'); }); - // @gate enableBinaryFlight it('useActionState can return binary state during MPA form submission', async () => { const serverAction = serverExports( async function action(prevState, formData) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index ada19fb1fc44b..4278c220d8b83 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -197,7 +197,6 @@ describe('ReactFlightDOMNode', () => { expect(result.text).toBe(testString); }); - // @gate enableBinaryFlight it('should be able to serialize any kind of typed array', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index a02d2b0c0fd81..2697d3eeb2dc8 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -53,7 +53,6 @@ describe('ReactFlightDOMReplyEdge', () => { expect(decoded).toEqual({some: 'object'}); }); - // @gate enableBinaryFlight it('should be able to serialize any kind of typed array', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -85,7 +84,6 @@ describe('ReactFlightDOMReplyEdge', () => { expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0])); }); - // @gate enableBinaryFlight it('should be able to serialize a typed array inside a Map', async () => { const array = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -102,7 +100,6 @@ describe('ReactFlightDOMReplyEdge', () => { expect(result.get('array')).toEqual(array); }); - // @gate enableBinaryFlight it('should be able to serialize a blob', async () => { const bytes = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -150,7 +147,7 @@ describe('ReactFlightDOMReplyEdge', () => { expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer()); }); - // @gate enableFlightReadableStream && enableBinaryFlight + // @gate enableFlightReadableStream it('should supports ReadableStreams with typed arrays', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, @@ -197,7 +194,7 @@ describe('ReactFlightDOMReplyEdge', () => { expect(streamedBuffers).toEqual(buffers); }); - // @gate enableFlightReadableStream && enableBinaryFlight + // @gate enableFlightReadableStream it('should support BYOB binary ReadableStreams', async () => { const buffer = new Uint8Array([ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 95746e6e16464..d47a28d49d506 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -30,10 +30,7 @@ import { createTemporaryReference, registerTemporaryReference, } from './ReactFlightServerTemporaryReferences'; -import { - enableBinaryFlight, - enableFlightReadableStream, -} from 'shared/ReactFeatureFlags'; +import {enableFlightReadableStream} from 'shared/ReactFeatureFlags'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import hasOwnProperty from 'shared/hasOwnProperty'; @@ -1019,51 +1016,42 @@ function parseModelString( return BigInt(value.slice(2)); } } - if (enableBinaryFlight) { - switch (value[1]) { - case 'A': - return parseTypedArray(response, value, ArrayBuffer, 1, obj, key); - case 'O': - return parseTypedArray(response, value, Int8Array, 1, obj, key); - case 'o': - return parseTypedArray(response, value, Uint8Array, 1, obj, key); - case 'U': - return parseTypedArray( - response, - value, - Uint8ClampedArray, - 1, - obj, - key, - ); - case 'S': - return parseTypedArray(response, value, Int16Array, 2, obj, key); - case 's': - return parseTypedArray(response, value, Uint16Array, 2, obj, key); - case 'L': - return parseTypedArray(response, value, Int32Array, 4, obj, key); - case 'l': - return parseTypedArray(response, value, Uint32Array, 4, obj, key); - case 'G': - return parseTypedArray(response, value, Float32Array, 4, obj, key); - case 'g': - return parseTypedArray(response, value, Float64Array, 8, obj, key); - case 'M': - return parseTypedArray(response, value, BigInt64Array, 8, obj, key); - case 'm': - return parseTypedArray(response, value, BigUint64Array, 8, obj, key); - case 'V': - return parseTypedArray(response, value, DataView, 1, obj, key); - case 'B': { - // Blob - const id = parseInt(value.slice(2), 16); - const prefix = response._prefix; - const blobKey = prefix + id; - // We should have this backingEntry in the store already because we emitted - // it before referencing it. It should be a Blob. - const backingEntry: Blob = (response._formData.get(blobKey): any); - return backingEntry; - } + switch (value[1]) { + case 'A': + return parseTypedArray(response, value, ArrayBuffer, 1, obj, key); + case 'O': + return parseTypedArray(response, value, Int8Array, 1, obj, key); + case 'o': + return parseTypedArray(response, value, Uint8Array, 1, obj, key); + case 'U': + return parseTypedArray(response, value, Uint8ClampedArray, 1, obj, key); + case 'S': + return parseTypedArray(response, value, Int16Array, 2, obj, key); + case 's': + return parseTypedArray(response, value, Uint16Array, 2, obj, key); + case 'L': + return parseTypedArray(response, value, Int32Array, 4, obj, key); + case 'l': + return parseTypedArray(response, value, Uint32Array, 4, obj, key); + case 'G': + return parseTypedArray(response, value, Float32Array, 4, obj, key); + case 'g': + return parseTypedArray(response, value, Float64Array, 8, obj, key); + case 'M': + return parseTypedArray(response, value, BigInt64Array, 8, obj, key); + case 'm': + return parseTypedArray(response, value, BigUint64Array, 8, obj, key); + case 'V': + return parseTypedArray(response, value, DataView, 1, obj, key); + case 'B': { + // Blob + const id = parseInt(value.slice(2), 16); + const prefix = response._prefix; + const blobKey = prefix + id; + // We should have this backingEntry in the store already because we emitted + // it before referencing it. It should be a Blob. + const backingEntry: Blob = (response._formData.get(blobKey): any); + return backingEntry; } } if (enableFlightReadableStream) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 860ff3cd0d898..7f8dd32a99921 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -14,7 +14,6 @@ import type {Postpone} from 'react/src/ReactPostpone'; import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences'; import { - enableBinaryFlight, enablePostpone, enableHalt, enableTaint, @@ -2696,63 +2695,60 @@ function renderModelDestructive( if (value instanceof Error) { return serializeErrorValue(request, value); } - - if (enableBinaryFlight) { - if (value instanceof ArrayBuffer) { - return serializeTypedArray(request, 'A', new Uint8Array(value)); - } - if (value instanceof Int8Array) { - // char - return serializeTypedArray(request, 'O', value); - } - if (value instanceof Uint8Array) { - // unsigned char - return serializeTypedArray(request, 'o', value); - } - if (value instanceof Uint8ClampedArray) { - // unsigned clamped char - return serializeTypedArray(request, 'U', value); - } - if (value instanceof Int16Array) { - // sort - return serializeTypedArray(request, 'S', value); - } - if (value instanceof Uint16Array) { - // unsigned short - return serializeTypedArray(request, 's', value); - } - if (value instanceof Int32Array) { - // long - return serializeTypedArray(request, 'L', value); - } - if (value instanceof Uint32Array) { - // unsigned long - return serializeTypedArray(request, 'l', value); - } - if (value instanceof Float32Array) { - // float - return serializeTypedArray(request, 'G', value); - } - if (value instanceof Float64Array) { - // double - return serializeTypedArray(request, 'g', value); - } - if (value instanceof BigInt64Array) { - // number - return serializeTypedArray(request, 'M', value); - } - if (value instanceof BigUint64Array) { - // unsigned number - // We use "m" instead of "n" since JSON can start with "null" - return serializeTypedArray(request, 'm', value); - } - if (value instanceof DataView) { - return serializeTypedArray(request, 'V', value); - } - // TODO: Blob is not available in old Node. Remove the typeof check later. - if (typeof Blob === 'function' && value instanceof Blob) { - return serializeBlob(request, value); - } + if (value instanceof ArrayBuffer) { + return serializeTypedArray(request, 'A', new Uint8Array(value)); + } + if (value instanceof Int8Array) { + // char + return serializeTypedArray(request, 'O', value); + } + if (value instanceof Uint8Array) { + // unsigned char + return serializeTypedArray(request, 'o', value); + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + return serializeTypedArray(request, 'U', value); + } + if (value instanceof Int16Array) { + // sort + return serializeTypedArray(request, 'S', value); + } + if (value instanceof Uint16Array) { + // unsigned short + return serializeTypedArray(request, 's', value); + } + if (value instanceof Int32Array) { + // long + return serializeTypedArray(request, 'L', value); + } + if (value instanceof Uint32Array) { + // unsigned long + return serializeTypedArray(request, 'l', value); + } + if (value instanceof Float32Array) { + // float + return serializeTypedArray(request, 'G', value); + } + if (value instanceof Float64Array) { + // double + return serializeTypedArray(request, 'g', value); + } + if (value instanceof BigInt64Array) { + // number + return serializeTypedArray(request, 'M', value); + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + return serializeTypedArray(request, 'm', value); + } + if (value instanceof DataView) { + return serializeTypedArray(request, 'V', value); + } + // TODO: Blob is not available in old Node. Remove the typeof check later. + if (typeof Blob === 'function' && value instanceof Blob) { + return serializeBlob(request, value); } const iteratorFn = getIteratorFn(value); @@ -3532,63 +3528,60 @@ function renderConsoleValue( if (value instanceof Error) { return serializeErrorValue(request, value); } - - if (enableBinaryFlight) { - if (value instanceof ArrayBuffer) { - return serializeTypedArray(request, 'A', new Uint8Array(value)); - } - if (value instanceof Int8Array) { - // char - return serializeTypedArray(request, 'O', value); - } - if (value instanceof Uint8Array) { - // unsigned char - return serializeTypedArray(request, 'o', value); - } - if (value instanceof Uint8ClampedArray) { - // unsigned clamped char - return serializeTypedArray(request, 'U', value); - } - if (value instanceof Int16Array) { - // sort - return serializeTypedArray(request, 'S', value); - } - if (value instanceof Uint16Array) { - // unsigned short - return serializeTypedArray(request, 's', value); - } - if (value instanceof Int32Array) { - // long - return serializeTypedArray(request, 'L', value); - } - if (value instanceof Uint32Array) { - // unsigned long - return serializeTypedArray(request, 'l', value); - } - if (value instanceof Float32Array) { - // float - return serializeTypedArray(request, 'G', value); - } - if (value instanceof Float64Array) { - // double - return serializeTypedArray(request, 'g', value); - } - if (value instanceof BigInt64Array) { - // number - return serializeTypedArray(request, 'M', value); - } - if (value instanceof BigUint64Array) { - // unsigned number - // We use "m" instead of "n" since JSON can start with "null" - return serializeTypedArray(request, 'm', value); - } - if (value instanceof DataView) { - return serializeTypedArray(request, 'V', value); - } - // TODO: Blob is not available in old Node. Remove the typeof check later. - if (typeof Blob === 'function' && value instanceof Blob) { - return serializeBlob(request, value); - } + if (value instanceof ArrayBuffer) { + return serializeTypedArray(request, 'A', new Uint8Array(value)); + } + if (value instanceof Int8Array) { + // char + return serializeTypedArray(request, 'O', value); + } + if (value instanceof Uint8Array) { + // unsigned char + return serializeTypedArray(request, 'o', value); + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + return serializeTypedArray(request, 'U', value); + } + if (value instanceof Int16Array) { + // sort + return serializeTypedArray(request, 'S', value); + } + if (value instanceof Uint16Array) { + // unsigned short + return serializeTypedArray(request, 's', value); + } + if (value instanceof Int32Array) { + // long + return serializeTypedArray(request, 'L', value); + } + if (value instanceof Uint32Array) { + // unsigned long + return serializeTypedArray(request, 'l', value); + } + if (value instanceof Float32Array) { + // float + return serializeTypedArray(request, 'G', value); + } + if (value instanceof Float64Array) { + // double + return serializeTypedArray(request, 'g', value); + } + if (value instanceof BigInt64Array) { + // number + return serializeTypedArray(request, 'M', value); + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + return serializeTypedArray(request, 'm', value); + } + if (value instanceof DataView) { + return serializeTypedArray(request, 'V', value); + } + // TODO: Blob is not available in old Node. Remove the typeof check later. + if (typeof Blob === 'function' && value instanceof Blob) { + return serializeBlob(request, value); } const iteratorFn = getIteratorFn(value); @@ -3878,71 +3871,69 @@ function emitChunk( emitTextChunk(request, id, value); return; } - if (enableBinaryFlight) { - if (value instanceof ArrayBuffer) { - emitTypedArrayChunk(request, id, 'A', new Uint8Array(value)); - return; - } - if (value instanceof Int8Array) { - // char - emitTypedArrayChunk(request, id, 'O', value); - return; - } - if (value instanceof Uint8Array) { - // unsigned char - emitTypedArrayChunk(request, id, 'o', value); - return; - } - if (value instanceof Uint8ClampedArray) { - // unsigned clamped char - emitTypedArrayChunk(request, id, 'U', value); - return; - } - if (value instanceof Int16Array) { - // sort - emitTypedArrayChunk(request, id, 'S', value); - return; - } - if (value instanceof Uint16Array) { - // unsigned short - emitTypedArrayChunk(request, id, 's', value); - return; - } - if (value instanceof Int32Array) { - // long - emitTypedArrayChunk(request, id, 'L', value); - return; - } - if (value instanceof Uint32Array) { - // unsigned long - emitTypedArrayChunk(request, id, 'l', value); - return; - } - if (value instanceof Float32Array) { - // float - emitTypedArrayChunk(request, id, 'G', value); - return; - } - if (value instanceof Float64Array) { - // double - emitTypedArrayChunk(request, id, 'g', value); - return; - } - if (value instanceof BigInt64Array) { - // number - emitTypedArrayChunk(request, id, 'M', value); - return; - } - if (value instanceof BigUint64Array) { - // unsigned number - // We use "m" instead of "n" since JSON can start with "null" - emitTypedArrayChunk(request, id, 'm', value); - return; - } - if (value instanceof DataView) { - emitTypedArrayChunk(request, id, 'V', value); - return; - } + if (value instanceof ArrayBuffer) { + emitTypedArrayChunk(request, id, 'A', new Uint8Array(value)); + return; + } + if (value instanceof Int8Array) { + // char + emitTypedArrayChunk(request, id, 'O', value); + return; + } + if (value instanceof Uint8Array) { + // unsigned char + emitTypedArrayChunk(request, id, 'o', value); + return; + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + emitTypedArrayChunk(request, id, 'U', value); + return; + } + if (value instanceof Int16Array) { + // sort + emitTypedArrayChunk(request, id, 'S', value); + return; + } + if (value instanceof Uint16Array) { + // unsigned short + emitTypedArrayChunk(request, id, 's', value); + return; + } + if (value instanceof Int32Array) { + // long + emitTypedArrayChunk(request, id, 'L', value); + return; + } + if (value instanceof Uint32Array) { + // unsigned long + emitTypedArrayChunk(request, id, 'l', value); + return; + } + if (value instanceof Float32Array) { + // float + emitTypedArrayChunk(request, id, 'G', value); + return; + } + if (value instanceof Float64Array) { + // double + emitTypedArrayChunk(request, id, 'g', value); + return; + } + if (value instanceof BigInt64Array) { + // number + emitTypedArrayChunk(request, id, 'M', value); + return; + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + emitTypedArrayChunk(request, id, 'm', value); + return; + } + if (value instanceof DataView) { + emitTypedArrayChunk(request, id, 'V', value); + return; } // For anything else we need to try to serialize it using JSON. // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 1664578317238..99bf73ac25448 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -7,7 +7,7 @@ * @flow */ -import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags'; +import {enableTaint} from 'shared/ReactFeatureFlags'; import getPrototypeOf from 'shared/getPrototypeOf'; @@ -76,8 +76,8 @@ export function taintUniqueValue( // Use as is. entryValue = value; } else if ( - enableBinaryFlight && - (value instanceof TypedArrayConstructor || value instanceof DataView) + value instanceof TypedArrayConstructor || + value instanceof DataView ) { // For now, we just convert binary data to a string so that we can just use the native // hashing in the Map implementation. It doesn't really matter what form the string diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a96d0e1917434..ccf9310ba5805 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -78,7 +78,6 @@ export const enableLegacyFBSupport = false; export const enableCache = true; export const enableLegacyCache = __EXPERIMENTAL__; -export const enableBinaryFlight = true; export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index eb3f5f3cd5553..0d13fe48058af 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -44,7 +44,6 @@ export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; -export const enableBinaryFlight = true; export const enableCache = true; export const enableComponentStackLocations = true; export const enableCPUSuspense = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index d725ecba679ce..a3ac1e88c2a97 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -31,7 +31,6 @@ export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; -export const enableBinaryFlight = true; export const enableCache = true; export const enableComponentStackLocations = true; export const enableCPUSuspense = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 2c6892de26d96..7d3502cb1cac0 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -21,7 +21,6 @@ export const enableComponentPerformanceTrack = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableLegacyCache = __EXPERIMENTAL__; -export const enableBinaryFlight = true; export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = false; export const enableTaint = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index eff87c5683fc8..92034467fd72b 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -23,7 +23,6 @@ export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; -export const enableBinaryFlight = true; export const enableCache = true; export const enableComponentStackLocations = true; export const enableCPUSuspense = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 00a0463353dda..b093ebf81b202 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -21,7 +21,6 @@ export const enableComponentPerformanceTrack = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableLegacyCache = true; -export const enableBinaryFlight = true; export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = false; export const enableTaint = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 65edbea8e153a..8abe581e7989c 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -76,7 +76,6 @@ export const enableGetInspectorDataForInstanceInProduction = false; export const enableCache = true; export const enableLegacyCache = true; -export const enableBinaryFlight = true; export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = false; From 982cf95c8b1914f87c5b1ea4ed73528e9d5dfb66 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 13 Dec 2024 15:49:06 -0500 Subject: [PATCH 3/6] Add --cleanup option to flags script to show groups of flags by status (#31762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `yarn flags --cleanup` will categorize flags to make it more clear which ones may need to be cleaned up, experiments checked on, or are blocked by internal rollouts. Alternative to #31760 Screenshot 2024-12-13 at 2 31 30 PM --- scripts/flags/flags.js | 217 +++++++++++++++++++++++++++++++++-------- 1 file changed, 174 insertions(+), 43 deletions(-) diff --git a/scripts/flags/flags.js b/scripts/flags/flags.js index a578304199a2c..1130ce80bb073 100644 --- a/scripts/flags/flags.js +++ b/scripts/flags/flags.js @@ -62,6 +62,12 @@ const argv = yargs 'experimental', ], }, + cleanup: { + describe: 'output flags by cleanup category.', + requiresArg: false, + type: 'boolean', + default: false, + }, }).argv; // Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'. @@ -375,53 +381,86 @@ const FLAG_CONFIG = { const FLAG_COLUMNS = Object.keys(FLAG_CONFIG); +const INTERNAL_VARIANTS = ['WWW Classic', 'WWW Modern', 'RN FB']; +const OSS_VARIANTS = [ + 'OSS Next Major', + 'OSS Canary', + 'OSS Experimental', + 'RN OSS', + 'RN Next Major', +]; + // Build the table with the value for each flag. -const isDiff = argv.diff != null && argv.diff.length > 1; -const table = {}; -// eslint-disable-next-line no-for-of-loops/no-for-of-loops -for (const flag of allFlagsUniqueFlags) { - const values = FLAG_COLUMNS.reduce((acc, key) => { - acc[key] = FLAG_CONFIG[key](flag); - return acc; - }, {}); - - if (!isDiff) { - table[flag] = values; - continue; - } +function buildTable(filterFn) { + const isDiff = argv.diff != null && argv.diff.length > 1; + const table = {}; + const filteredFlags = filterFn + ? allFlagsUniqueFlags.filter(filterFn) + : allFlagsUniqueFlags; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const flag of filteredFlags) { + const values = FLAG_COLUMNS.reduce((acc, key) => { + acc[key] = FLAG_CONFIG[key](flag); + return acc; + }, {}); + + if (!isDiff) { + table[flag] = values; + continue; + } + + const subset = argv.diff.map(argToHeader).reduce((acc, key) => { + if (key in values) { + acc[key] = values[key]; + } + return acc; + }, {}); - const subset = argv.diff.map(argToHeader).reduce((acc, key) => { - if (key in values) { - acc[key] = values[key]; + if (new Set(Object.values(subset)).size !== 1) { + table[flag] = subset; } - return acc; - }, {}); + } - if (new Set(Object.values(subset)).size !== 1) { - table[flag] = subset; + // Sort the table + let sorted = table; + if (isDiff || argv.sort) { + const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort); + const sortBy = + sortChannel === 'flag' + ? ([flagA], [flagB]) => { + return flagA.localeCompare(flagB); + } + : ([, rowA], [, rowB]) => { + return rowB[sortChannel] + .toString() + .localeCompare(rowA[sortChannel]); + }; + sorted = Object.fromEntries(Object.entries(table).sort(sortBy)); } + + return sorted; } -// Sort the table -let sorted = table; -if (isDiff || argv.sort) { - const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort); - const sortBy = - sortChannel === 'flag' - ? ([flagA], [flagB]) => { - return flagA.localeCompare(flagB); - } - : ([, rowA], [, rowB]) => { - return rowB[sortChannel].toString().localeCompare(rowA[sortChannel]); - }; - sorted = Object.fromEntries(Object.entries(table).sort(sortBy)); +function formatTable(tableData) { + // left align the flag names. + const maxLength = Math.max( + ...Object.keys(tableData).map(item => item.length) + ); + const padded = {}; + Object.keys(tableData).forEach(key => { + const newKey = key.padEnd(maxLength, ' '); + padded[newKey] = tableData[key]; + }); + + return padded; } if (argv.csv) { + const table = buildTable(); const csvRows = [ `Flag name, ${FLAG_COLUMNS.join(', ')}`, ...Object.keys(table).map(flag => { - const row = sorted[flag]; + const row = table[flag]; return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`; }), ]; @@ -433,16 +472,108 @@ if (argv.csv) { }); } -// left align the flag names. -const maxLength = Math.max(...Object.keys(sorted).map(item => item.length)); -const padded = {}; -Object.keys(sorted).forEach(key => { - const newKey = key.padEnd(maxLength, ' '); - padded[newKey] = sorted[key]; -}); +if (argv.cleanup) { + const allPassingFlags = []; + const allFailingFlags = []; + const needsShippedExperimentFlags = []; + const earlyExperimentationFlags = []; + const internalOnlyFlags = []; + + const diffedFlagColumns = + argv.diff[0] != null ? argv.diff.map(argToHeader) : FLAG_COLUMNS; + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const flag of allFlagsUniqueFlags) { + const values = diffedFlagColumns.reduce((acc, key) => { + acc[key] = FLAG_CONFIG[key](flag); + return acc; + }, {}); + + const uniqueValues = new Set(Object.values(values)); + + if ( + uniqueValues.size === 1 && + (uniqueValues.has('✅') || + typeof uniqueValues.values().next().value === 'number') + ) { + allPassingFlags.push(flag); + } + + if (uniqueValues.size === 1 && uniqueValues.has('❌')) { + allFailingFlags.push(flag); + } + + const internalVariantValues = INTERNAL_VARIANTS.filter(value => + diffedFlagColumns.includes(value) + ).map(v => values[v]); + const ossVariantValues = OSS_VARIANTS.filter(value => + diffedFlagColumns.includes(value) + ).map(v => values[v]); + + if ( + internalVariantValues.some(v => v === '✅') && + ossVariantValues.every(v => v === '❌') + ) { + internalOnlyFlags.push(flag); + } + + if ( + internalVariantValues.some(v => v === '🧪') && + (ossVariantValues.every(v => v === '❌') || + (ossVariantValues.some(v => v === '❌') && + values['OSS Experimental'] === '✅')) + ) { + earlyExperimentationFlags.push(flag); + } + + if ( + internalVariantValues.some(v => v === '🧪' || v === '❌') && + ossVariantValues.every(v => v === '✅') + ) { + needsShippedExperimentFlags.push(flag); + } + } + + if (allPassingFlags.length > 0) { + console.log('ALL VARIANTS PASS (✅)'); + console.table( + formatTable(buildTable(flag => allPassingFlags.includes(flag))) + ); + } + + if (allFailingFlags.length > 0) { + console.log('ALL VARIANTS FAIL (❌)'); + console.table( + formatTable(buildTable(flag => allFailingFlags.includes(flag))) + ); + } + + if (internalOnlyFlags.length > 0) { + console.log('INTERNAL ONLY (✅)'); + console.table( + formatTable(buildTable(flag => internalOnlyFlags.includes(flag))) + ); + } + + if (earlyExperimentationFlags.length > 0) { + console.log('WAITING ON RESULTS (🧪)'); + console.table( + formatTable(buildTable(flag => earlyExperimentationFlags.includes(flag))) + ); + } + + if (needsShippedExperimentFlags.length > 0) { + console.log('WAITING ON ROLLOUT (🧪)'); + console.table( + formatTable( + buildTable(flag => needsShippedExperimentFlags.includes(flag)) + ) + ); + } +} else { + console.table(formatTable(buildTable())); +} -// print table with formatting -console.table(padded); console.log(` Legend: ✅ On From 3ad17ecd313a8e53b339adf8052e35b3d73f8c62 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 13 Dec 2024 15:52:42 -0500 Subject: [PATCH 4/6] Remove enableComponentStackLocations (#31764) This has landed everywhere --- packages/shared/ReactComponentStackFrame.js | 68 +++------ packages/shared/ReactFeatureFlags.js | 2 +- .../__tests__/describeComponentFrame-test.js | 137 ------------------ .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 2 - 9 files changed, 22 insertions(+), 192 deletions(-) delete mode 100644 packages/shared/__tests__/describeComponentFrame-test.js diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index 96f69617e607b..a276d87154d52 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -9,8 +9,6 @@ import type {LazyComponent} from 'react/src/ReactLazy'; -import {enableComponentStackLocations} from 'shared/ReactFeatureFlags'; - import { REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, @@ -28,30 +26,26 @@ import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace'; let prefix; let suffix; export function describeBuiltInComponentFrame(name: string): string { - if (enableComponentStackLocations) { - if (prefix === undefined) { - // Extract the VM specific prefix used by each line. - try { - throw Error(); - } catch (x) { - const match = x.stack.trim().match(/\n( *(at )?)/); - prefix = (match && match[1]) || ''; - suffix = - x.stack.indexOf('\n at') > -1 - ? // V8 - ' ()' - : // JSC/Spidermonkey - x.stack.indexOf('@') > -1 - ? '@unknown:0:0' - : // Other - ''; - } + if (prefix === undefined) { + // Extract the VM specific prefix used by each line. + try { + throw Error(); + } catch (x) { + const match = x.stack.trim().match(/\n( *(at )?)/); + prefix = (match && match[1]) || ''; + suffix = + x.stack.indexOf('\n at') > -1 + ? // V8 + ' ()' + : // JSC/Spidermonkey + x.stack.indexOf('@') > -1 + ? '@unknown:0:0' + : // Other + ''; } - // We use the prefix to ensure our stacks line up with native stack frames. - return '\n' + prefix + name + suffix; - } else { - return describeComponentFrame(name); } + // We use the prefix to ensure our stacks line up with native stack frames. + return '\n' + prefix + name + suffix; } export function describeDebugInfoFrame(name: string, env: ?string): string { @@ -296,28 +290,12 @@ export function describeNativeComponentFrame( return syntheticFrame; } -function describeComponentFrame(name: null | string) { - return '\n in ' + (name || 'Unknown'); -} - export function describeClassComponentFrame(ctor: Function): string { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(ctor, true); - } else { - return describeFunctionComponentFrame(ctor); - } + return describeNativeComponentFrame(ctor, true); } export function describeFunctionComponentFrame(fn: Function): string { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(fn, false); - } else { - if (!fn) { - return ''; - } - const name = fn.displayName || fn.name || null; - return describeComponentFrame(name); - } + return describeNativeComponentFrame(fn, false); } function shouldConstruct(Component: Function) { @@ -334,11 +312,7 @@ export function describeUnknownElementTypeFrameInDEV(type: any): string { return ''; } if (typeof type === 'function') { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(type, shouldConstruct(type)); - } else { - return describeFunctionComponentFrame(type); - } + return describeNativeComponentFrame(type, shouldConstruct(type)); } if (typeof type === 'string') { return describeBuiltInComponentFrame(type); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ccf9310ba5805..d82b3570118d9 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -13,7 +13,7 @@ // Flags that can likely be deleted or landed without consequences // ----------------------------------------------------------------------------- -export const enableComponentStackLocations = true; +// None // ----------------------------------------------------------------------------- // Killswitch diff --git a/packages/shared/__tests__/describeComponentFrame-test.js b/packages/shared/__tests__/describeComponentFrame-test.js deleted file mode 100644 index 6fc5bef2c9945..0000000000000 --- a/packages/shared/__tests__/describeComponentFrame-test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactDOMClient; -let act; -let jsxDEV; - -describe('Component stack trace displaying', () => { - beforeEach(() => { - React = require('react'); - ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; - jsxDEV = require('react/jsx-dev-runtime').jsxDEV; - }); - - // @gate !enableComponentStackLocations - // @gate __DEV__ - it('should provide filenames in stack traces', async () => { - class Component extends React.Component { - render() { - return [a, b]; - } - } - - spyOnDev(console, 'error'); - const container = document.createElement('div'); - const fileNames = { - '': '', - '/': '', - '\\': '', - Foo: 'Foo', - 'Bar/Foo': 'Foo', - 'Bar\\Foo': 'Foo', - 'Baz/Bar/Foo': 'Foo', - 'Baz\\Bar\\Foo': 'Foo', - - 'Foo.js': 'Foo.js', - 'Foo.jsx': 'Foo.jsx', - '/Foo.js': 'Foo.js', - '/Foo.jsx': 'Foo.jsx', - '\\Foo.js': 'Foo.js', - '\\Foo.jsx': 'Foo.jsx', - 'Bar/Foo.js': 'Foo.js', - 'Bar/Foo.jsx': 'Foo.jsx', - 'Bar\\Foo.js': 'Foo.js', - 'Bar\\Foo.jsx': 'Foo.jsx', - '/Bar/Foo.js': 'Foo.js', - '/Bar/Foo.jsx': 'Foo.jsx', - '\\Bar\\Foo.js': 'Foo.js', - '\\Bar\\Foo.jsx': 'Foo.jsx', - 'Bar/Baz/Foo.js': 'Foo.js', - 'Bar/Baz/Foo.jsx': 'Foo.jsx', - 'Bar\\Baz\\Foo.js': 'Foo.js', - 'Bar\\Baz\\Foo.jsx': 'Foo.jsx', - '/Bar/Baz/Foo.js': 'Foo.js', - '/Bar/Baz/Foo.jsx': 'Foo.jsx', - '\\Bar\\Baz\\Foo.js': 'Foo.js', - '\\Bar\\Baz\\Foo.jsx': 'Foo.jsx', - 'C:\\funny long (path)/Foo.js': 'Foo.js', - 'C:\\funny long (path)/Foo.jsx': 'Foo.jsx', - - 'index.js': 'index.js', - 'index.jsx': 'index.jsx', - '/index.js': 'index.js', - '/index.jsx': 'index.jsx', - '\\index.js': 'index.js', - '\\index.jsx': 'index.jsx', - 'Bar/index.js': 'Bar/index.js', - 'Bar/index.jsx': 'Bar/index.jsx', - 'Bar\\index.js': 'Bar/index.js', - 'Bar\\index.jsx': 'Bar/index.jsx', - '/Bar/index.js': 'Bar/index.js', - '/Bar/index.jsx': 'Bar/index.jsx', - '\\Bar\\index.js': 'Bar/index.js', - '\\Bar\\index.jsx': 'Bar/index.jsx', - 'Bar/Baz/index.js': 'Baz/index.js', - 'Bar/Baz/index.jsx': 'Baz/index.jsx', - 'Bar\\Baz\\index.js': 'Baz/index.js', - 'Bar\\Baz\\index.jsx': 'Baz/index.jsx', - '/Bar/Baz/index.js': 'Baz/index.js', - '/Bar/Baz/index.jsx': 'Baz/index.jsx', - '\\Bar\\Baz\\index.js': 'Baz/index.js', - '\\Bar\\Baz\\index.jsx': 'Baz/index.jsx', - 'C:\\funny long (path)/index.js': 'funny long (path)/index.js', - 'C:\\funny long (path)/index.jsx': 'funny long (path)/index.jsx', - }; - - const root = ReactDOMClient.createRoot(container); - - let i = 0; - for (const fileName in fileNames) { - Component.displayName = 'Component ' + i; - - await act(() => { - root.render( - // Intentionally inlining a manual jsxDEV() instead of relying on the - // compiler so that we can pass a custom source location. - jsxDEV( - Component, - {}, - undefined, - false, - {fileName, lineNumber: i}, - this, - ), - ); - }); - - i++; - } - if (__DEV__) { - i = 0; - expect(console.error).toHaveBeenCalledTimes( - Object.keys(fileNames).length, - ); - for (const fileName in fileNames) { - if (!fileNames.hasOwnProperty(fileName)) { - continue; - } - const args = console.error.mock.calls[i]; - const stack = args[args.length - 1]; - const expected = fileNames[fileName]; - expect(stack).toContain(`at ${expected}:`); - i++; - } - } - }); -}); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0d13fe48058af..f2b80a9b9f0f2 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -45,7 +45,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a3ac1e88c2a97..e5d233c61181a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -32,7 +32,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = false; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7d3502cb1cac0..fc147e0d12329 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -40,7 +40,6 @@ export const enableUseMemoCacheHook = true; export const enableNoCloningMemoCache = false; export const enableUseEffectEventHook = false; export const favorSafetyOverHydrationPerf = true; -export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableMoveBefore = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 92034467fd72b..8cdfc474647d1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -24,7 +24,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index b093ebf81b202..960c41199128c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -42,7 +42,6 @@ export const enableUseMemoCacheHook = true; export const enableNoCloningMemoCache = false; export const enableUseEffectEventHook = false; export const favorSafetyOverHydrationPerf = true; -export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableMoveBefore = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 8abe581e7989c..903b75e6e3577 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -99,8 +99,6 @@ export const enableSuspenseCallback = true; export const enableLegacyHidden = true; -export const enableComponentStackLocations = true; - export const disableTextareaChildren = __EXPERIMENTAL__; export const enableFizzExternalRuntime = true; From 4996a8fa5c5bf9e12e750c46b48f25656fb050cf Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 13 Dec 2024 16:30:10 -0500 Subject: [PATCH 5/6] Remove enableFilterEmptyStringAttributesDOM (#31765) Base off https://github.com/facebook/react/pull/31764 This has landed everywhere --- .../src/client/ReactDOMComponent.js | 144 ++++++------ .../src/server/ReactFizzConfigDOM.js | 75 +++---- .../src/__tests__/ReactDOMComponent-test.js | 208 +++++++++--------- .../src/__tests__/ReactDOMFloat-test.js | 4 +- ...eactDOMServerIntegrationAttributes-test.js | 16 +- packages/shared/ReactFeatureFlags.js | 5 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 12 files changed, 209 insertions(+), 249 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index ac95c91c596a8..05892c930e1ca 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -63,10 +63,7 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import sanitizeURL from '../shared/sanitizeURL'; -import { - enableTrustedTypesIntegration, - enableFilterEmptyStringAttributesDOM, -} from 'shared/ReactFeatureFlags'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import { mediaEventTypes, listenToNonDelegatedEvent, @@ -400,35 +397,33 @@ function setProp( // fallthrough case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && key === 'href') - ) { - if (__DEV__) { - if (key === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && key === 'href') + ) { + if (__DEV__) { + if (key === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); } - domElement.removeAttribute(key); - break; } + domElement.removeAttribute(key); + break; } if ( value == null || @@ -2489,53 +2484,52 @@ function diffHydratedGenericElement( // fallthrough case 'src': case 'href': - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && propKey === 'href') && - !(tag === 'object' && propKey === 'data') - ) { - if (__DEV__) { - if (propKey === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && propKey === 'href') && + !(tag === 'object' && propKey === 'data') + ) { + if (__DEV__) { + if (propKey === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - null, - extraAttributes, - serverDifferences, - ); - continue; } + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + null, + extraAttributes, + serverDifferences, + ); + continue; + } else { + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + serverDifferences, + ); + continue; } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - value, - extraAttributes, - serverDifferences, - ); - continue; case 'action': case 'formAction': { const serverValue = domElement.getAttribute(propKey); diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 1351a28db3aab..fe2e713d8a3ba 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -27,10 +27,7 @@ import { import {Children} from 'react'; -import { - enableFilterEmptyStringAttributesDOM, - enableFizzExternalRuntime, -} from 'shared/ReactFeatureFlags'; +import {enableFizzExternalRuntime} from 'shared/ReactFeatureFlags'; import type { Destination, @@ -1210,30 +1207,28 @@ function pushAttribute( } case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if (value === '') { - if (__DEV__) { - if (name === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } + if (value === '') { + if (__DEV__) { + if (name === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); } - return; } + return; } } // Fall through to the last case which shouldn't remove empty strings. @@ -1633,19 +1628,17 @@ function pushStartObject( checkAttributeStringCoercion(propValue, 'data'); } const sanitizedValue = sanitizeURL('' + propValue); - if (enableFilterEmptyStringAttributesDOM) { - if (sanitizedValue === '') { - if (__DEV__) { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } - break; + if (sanitizedValue === '') { + if (__DEV__) { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } + break; } target.push( attributeSeparator, @@ -3615,11 +3608,7 @@ export function pushStartInstance( // Fast track very common tags break; case 'a': - if (enableFilterEmptyStringAttributesDOM) { - return pushStartAnchor(target, props); - } else { - break; - } + return pushStartAnchor(target, props); case 'g': case 'p': case 'li': diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index d37a4ecba6dc6..ce71a6334ee64 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -587,133 +587,131 @@ describe('ReactDOMComponent', () => { expect(node.hasAttribute('data-foo')).toBe(false); }); - if (ReactFeatureFlags.enableFilterEmptyStringAttributesDOM) { - it('should not add an empty src attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('src')).toBe(false); - + it('should not add an empty src attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('src')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('src')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - expect(node.hasAttribute('src')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('src')).toBe(true); - it('should not add an empty href attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('href')).toBe(false); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + expect(node.hasAttribute('src')).toBe(false); + }); + it('should not add an empty href attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('href')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('href')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - expect(node.hasAttribute('href')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('href')).toBe(true); - it('should allow an empty href attribute on anchors', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - const node = container.firstChild; - expect(node.getAttribute('href')).toBe(''); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + expect(node.hasAttribute('href')).toBe(false); + }); + + it('should allow an empty href attribute on anchors', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); }); + const node = container.firstChild; + expect(node.getAttribute('href')).toBe(''); + }); - it('should allow an empty action attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - const node = container.firstChild; - expect(node.getAttribute('action')).toBe(''); + it('should allow an empty action attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + const node = container.firstChild; + expect(node.getAttribute('action')).toBe(''); - await act(() => { - root.render(); - }); - expect(node.hasAttribute('action')).toBe(true); + await act(() => { + root.render(); + }); + expect(node.hasAttribute('action')).toBe(true); - await act(() => { - root.render(); - }); - expect(node.getAttribute('action')).toBe(''); + await act(() => { + root.render(); }); + expect(node.getAttribute('action')).toBe(''); + }); - it('allows empty string of a formAction to override the default of a parent', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - -