From 30a81df1c1846e6f9e9c92815e0f5fe7eca6a449 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 10:25:15 -0500 Subject: [PATCH 1/8] Remove consoleManagedByDevToolsDuringStrictMode --- .../src/ReactFiberDevToolsHook.js | 42 +- .../src/__tests__/ReactStrictMode-test.js | 594 ++++++------------ packages/shared/ReactFeatureFlags.js | 2 - .../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 | 2 - 9 files changed, 201 insertions(+), 446 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.js index 25ee5ee70cf88..7a0c7946bf277 100644 --- a/packages/react-reconciler/src/ReactFiberDevToolsHook.js +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.js @@ -19,7 +19,6 @@ type DevToolsProfilingHooks = any; import {DidCapture} from './ReactFiberFlags'; import { - consoleManagedByDevToolsDuringStrictMode, enableProfilerTimer, enableSchedulingProfiler, } from 'shared/ReactFeatureFlags'; @@ -188,36 +187,25 @@ export function onCommitUnmount(fiber: Fiber) { } export function setIsStrictModeForDevtools(newIsStrictMode: boolean) { - if (consoleManagedByDevToolsDuringStrictMode) { - if (typeof log === 'function') { - // We're in a test because Scheduler.log only exists - // in SchedulerMock. To reduce the noise in strict mode tests, - // suppress warnings and disable scheduler yielding during the double render - unstable_setDisableYieldValue(newIsStrictMode); - setSuppressWarning(newIsStrictMode); - } + if (typeof log === 'function') { + // We're in a test because Scheduler.log only exists + // in SchedulerMock. To reduce the noise in strict mode tests, + // suppress warnings and disable scheduler yielding during the double render + unstable_setDisableYieldValue(newIsStrictMode); + setSuppressWarning(newIsStrictMode); + } - if (injectedHook && typeof injectedHook.setStrictMode === 'function') { - try { - injectedHook.setStrictMode(rendererID, newIsStrictMode); - } catch (err) { - if (__DEV__) { - if (!hasLoggedError) { - hasLoggedError = true; - console.error( - 'React instrumentation encountered an error: %s', - err, - ); - } + if (injectedHook && typeof injectedHook.setStrictMode === 'function') { + try { + injectedHook.setStrictMode(rendererID, newIsStrictMode); + } catch (err) { + if (__DEV__) { + if (!hasLoggedError) { + hasLoggedError = true; + console.error('React instrumentation encountered an error: %s', err); } } } - } else { - if (newIsStrictMode) { - disableLogs(); - } else { - reenableLogs(); - } } } diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index db60674ba64bf..5b5d673e22dfb 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -1111,450 +1111,228 @@ describe('context legacy', () => { console.log.mockRestore(); }); - if (ReactFeatureFlags.consoleManagedByDevToolsDuringStrictMode) { - it('does not disable logs for class double render', async () => { - let count = 0; - class Foo extends React.Component { - render() { - count++; - console.log('foo ' + count); - return null; - } + it('does not disable logs for class double render', async () => { + let count = 0; + class Foo extends React.Component { + render() { + count++; + console.log('foo ' + count); + return null; } + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - it('does not disable logs for class double ctor', async () => { - let count = 0; - class Foo extends React.Component { - constructor(props) { - super(props); - count++; - console.log('foo ' + count); - } - render() { - return null; - } + it('does not disable logs for class double ctor', async () => { + let count = 0; + class Foo extends React.Component { + constructor(props) { + super(props); + count++; + console.log('foo ' + count); } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); - }); - - it('does not disable logs for class double getDerivedStateFromProps', async () => { - let count = 0; - class Foo extends React.Component { - state = {}; - static getDerivedStateFromProps() { - count++; - console.log('foo ' + count); - return {}; - } - render() { - return null; - } + render() { + return null; } + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - it('does not disable logs for class double shouldComponentUpdate', async () => { - let count = 0; - class Foo extends React.Component { - state = {}; - shouldComponentUpdate() { - count++; - console.log('foo ' + count); - return {}; - } - render() { - return null; - } + it('does not disable logs for class double getDerivedStateFromProps', async () => { + let count = 0; + class Foo extends React.Component { + state = {}; + static getDerivedStateFromProps() { + count++; + console.log('foo ' + count); + return {}; } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - await act(() => { - root.render( - - - , - ); - }); - - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); - }); - - it('does not disable logs for class state updaters', async () => { - let inst; - let count = 0; - class Foo extends React.Component { - state = {}; - render() { - inst = this; - return null; - } + render() { + return null; } + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - await act(() => { - inst.setState(() => { - count++; - console.log('foo ' + count); - return {}; - }); - }); - - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - it('does not disable logs for function double render', async () => { - let count = 0; - function Foo() { + it('does not disable logs for class double shouldComponentUpdate', async () => { + let count = 0; + class Foo extends React.Component { + state = {}; + shouldComponentUpdate() { count++; console.log('foo ' + count); - return null; + return {}; } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); - }); - - it('does not disable logs for effect double invoke', async () => { - let create = 0; - let cleanup = 0; - function Foo() { - React.useEffect(() => { - create++; - console.log('foo create ' + create); - return () => { - cleanup++; - console.log('foo cleanup ' + cleanup); - }; - }); + render() { return null; } + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(create).toBe(__DEV__ ? 2 : 1); - expect(cleanup).toBe(__DEV__ ? 1 : 0); - expect(console.log).toBeCalledTimes(__DEV__ ? 3 : 1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo create 1'); - if (__DEV__) { - expect(console.log).toBeCalledWith('foo cleanup 1'); - } + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); - } else { - it('disable logs for class double render', async () => { - let count = 0; - class Foo extends React.Component { - render() { - count++; - console.log('foo ' + count); - return null; - } - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + await act(() => { + root.render( + + + , + ); }); - it('disables logs for class double ctor', async () => { - let count = 0; - class Foo extends React.Component { - constructor(props) { - super(props); - count++; - console.log('foo ' + count); - } - render() { - return null; - } - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); - }); + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - it('disable logs for class double getDerivedStateFromProps', async () => { - let count = 0; - class Foo extends React.Component { - state = {}; - static getDerivedStateFromProps() { - count++; - console.log('foo ' + count); - return {}; - } - render() { - return null; - } + it('does not disable logs for class state updaters', async () => { + let inst; + let count = 0; + class Foo extends React.Component { + state = {}; + render() { + inst = this; + return null; } + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); - - it('disable logs for class double shouldComponentUpdate', async () => { - let count = 0; - class Foo extends React.Component { - state = {}; - shouldComponentUpdate() { - count++; - console.log('foo ' + count); - return {}; - } - render() { - return null; - } - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - await act(() => { - root.render( - - - , - ); + await act(() => { + inst.setState(() => { + count++; + console.log('foo ' + count); + return {}; }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); }); - it('disable logs for class state updaters', async () => { - let inst; - let count = 0; - class Foo extends React.Component { - state = {}; - render() { - inst = this; - return null; - } - } + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - await act(() => { - inst.setState(() => { - count++; - console.log('foo ' + count); - return {}; - }); - }); + it('does not disable logs for function double render', async () => { + let count = 0; + function Foo() { + count++; + console.log('foo ' + count); + return null; + } - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); + expect(count).toBe(__DEV__ ? 2 : 1); + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo 1'); + }); - it('disable logs for function double render', async () => { - let count = 0; - function Foo() { - count++; - console.log('foo ' + count); - return null; - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); + it('does not disable logs for effect double invoke', async () => { + let create = 0; + let cleanup = 0; + function Foo() { + React.useEffect(() => { + create++; + console.log('foo create ' + create); + return () => { + cleanup++; + console.log('foo cleanup ' + cleanup); + }; }); - expect(count).toBe(__DEV__ ? 2 : 1); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo 1'); - }); - - it('disable logs for effect double invoke', async () => { - let create = 0; - let cleanup = 0; - function Foo() { - React.useEffect(() => { - create++; - console.log('foo create ' + create); - return () => { - cleanup++; - console.log('foo cleanup ' + cleanup); - }; - }); - return null; - } + return null; + } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(create).toBe(__DEV__ ? 2 : 1); - expect(cleanup).toBe(__DEV__ ? 1 : 0); - expect(console.log).toBeCalledTimes(1); - // Note: we should display the first log because otherwise - // there is a risk of suppressing warnings when they happen, - // and on the next render they'd get deduplicated and ignored. - expect(console.log).toBeCalledWith('foo create 1'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); }); - } + expect(create).toBe(__DEV__ ? 2 : 1); + expect(cleanup).toBe(__DEV__ ? 1 : 0); + expect(console.log).toBeCalledTimes(__DEV__ ? 3 : 1); + // Note: we should display the first log because otherwise + // there is a risk of suppressing warnings when they happen, + // and on the next render they'd get deduplicated and ignored. + expect(console.log).toBeCalledWith('foo create 1'); + if (__DEV__) { + expect(console.log).toBeCalledWith('foo cleanup 1'); + } + }); }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 6ccf1ce8dd828..f8aadb9789406 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -297,6 +297,4 @@ export const enableUpdaterTracking = __PROFILE__; // Internal only. export const enableGetInspectorDataForInstanceInProduction = false; -export const consoleManagedByDevToolsDuringStrictMode = true; - export const enableDO_NOT_USE_disableStrictPassiveEffect = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 850249b8f79fd..37991d4c8ffbd 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -32,7 +32,6 @@ export const { } = dynamicFlags; // The rest of the flags are static for better dead code elimination. -export const consoleManagedByDevToolsDuringStrictMode = true; export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 419cee942a9cc..eb40dc9123752 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -20,7 +20,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; // All other flags // ----------------------------------------------------------------------------- export const alwaysThrottleRetries = false; -export const consoleManagedByDevToolsDuringStrictMode = true; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; export const disableDefaultPropsExceptForClasses = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index c591794c20e83..6673d056d9a09 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -61,8 +61,6 @@ export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableLegacyHidden = false; -export const consoleManagedByDevToolsDuringStrictMode = false; - export const enableTransitionTracing = false; export const useModernStrictMode = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 7eef951628bba..3ac86950277d4 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -11,7 +11,6 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; import typeof * as ExportsType from './ReactFeatureFlags.test-renderer'; export const alwaysThrottleRetries = false; -export const consoleManagedByDevToolsDuringStrictMode = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2ca5c62988aa8..8ebb4d0dbf8db 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -63,8 +63,6 @@ export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableLegacyHidden = false; -export const consoleManagedByDevToolsDuringStrictMode = false; - export const enableTransitionTracing = false; export const useModernStrictMode = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 40e996b571a96..7a5761932c820 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -106,8 +106,6 @@ export const enableComponentStackLocations = true; export const disableTextareaChildren = __EXPERIMENTAL__; -export const consoleManagedByDevToolsDuringStrictMode = true; - export const enableFizzExternalRuntime = true; export const passChildrenWhenCloningPersistedNodes = false; From e19682e3cd87e2b22527d1b941201181a6d9f987 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 11:00:13 -0500 Subject: [PATCH 2/8] fix lint --- packages/react-reconciler/src/ReactFiberDevToolsHook.js | 1 - packages/react/src/__tests__/ReactStrictMode-test.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.js index 7a0c7946bf277..98bad19a8e296 100644 --- a/packages/react-reconciler/src/ReactFiberDevToolsHook.js +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.js @@ -37,7 +37,6 @@ import { unstable_setDisableYieldValue, } from './Scheduler'; import {setSuppressWarning} from 'shared/consoleWithStackDev'; -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; declare const __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void; diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index 5b5d673e22dfb..863f84ebdb291 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -20,8 +20,6 @@ let useState; let useReducer; let assertConsoleErrorDev; -const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - describe('ReactStrictMode', () => { beforeEach(() => { jest.resetModules(); From 4fc2cfdd2364d21ea186cbde04be8b71872b9a2a Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 10:30:06 -0500 Subject: [PATCH 3/8] Remove disableIEWorkarounds --- .../src/client/ReactDOMComponent.js | 58 ++++++------------- .../__tests__/dangerouslySetInnerHTML-test.js | 37 ------------ .../__tests__/trustedTypes-test.internal.js | 23 -------- packages/shared/ReactFeatureFlags.js | 4 -- .../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 - scripts/jest/setupTests.www.js | 1 - 11 files changed, 17 insertions(+), 112 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 9f7b23026f758..b62ad0bf62b74 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -66,7 +66,6 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO import sanitizeURL from '../shared/sanitizeURL'; import { - disableIEWorkarounds, enableTrustedTypesIntegration, enableFilterEmptyStringAttributesDOM, } from 'shared/ReactFeatureFlags'; @@ -83,19 +82,8 @@ let didWarnFormActionTarget = false; let didWarnFormActionMethod = false; let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean}; let didWarnPopoverTargetObject = false; -let canDiffStyleForHydrationWarning; if (__DEV__) { didWarnForNewBooleanPropsWithEmptyValue = {}; - // IE 11 parses & normalizes the style attribute as opposed to other - // browsers. It adds spaces and sorts the properties in some - // non-alphabetical order. Handling that would require sorting CSS - // properties in the client & server versions or applying - // `expectedStyle` to a temporary DOM node to read its `style` attribute - // normalized. Since it only affects IE, we're skipping style warnings - // in that browser completely in favor of doing all that work. - // See https://github.com/facebook/react/issues/11807 - canDiffStyleForHydrationWarning = - disableIEWorkarounds || (canUseDOM && !document.documentMode); } function validatePropertiesInDevelopment(type: string, props: any) { @@ -579,11 +567,7 @@ function setProp( 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', ); } - if (disableIEWorkarounds) { - domElement.innerHTML = nextHtml; - } else { - setInnerHTML(domElement, nextHtml); - } + domElement.innerHTML = nextHtml; } } break; @@ -939,11 +923,7 @@ function setPropOnCustomElement( 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', ); } - if (disableIEWorkarounds) { - domElement.innerHTML = nextHtml; - } else { - setInnerHTML(domElement, nextHtml); - } + domElement.innerHTML = nextHtml; } } break; @@ -1931,27 +1911,23 @@ function diffHydratedStyles( } return; } - if (canDiffStyleForHydrationWarning) { - // First we compare the string form and see if it's equivalent. - // This lets us bail out on anything that used to pass in this form. - // It also lets us compare anything that's not parsed by this browser. - const clientValue = createDangerousStringForStyles(value); - const serverValue = domElement.getAttribute('style'); + // First we compare the string form and see if it's equivalent. + // This lets us bail out on anything that used to pass in this form. + // It also lets us compare anything that's not parsed by this browser. + const clientValue = createDangerousStringForStyles(value); + const serverValue = domElement.getAttribute('style'); - if (serverValue === clientValue) { - return; - } - const normalizedClientValue = - normalizeMarkupForTextOrAttribute(clientValue); - const normalizedServerValue = - normalizeMarkupForTextOrAttribute(serverValue); - if (normalizedServerValue === normalizedClientValue) { - return; - } - - // Otherwise, we create the object from the DOM for the diff view. - serverDifferences.style = getStylesObjectFromElement(domElement); + if (serverValue === clientValue) { + return; + } + const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue); + const normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue); + if (normalizedServerValue === normalizedClientValue) { + return; } + + // Otherwise, we create the object from the DOM for the diff view. + serverDifferences.style = getStylesObjectFromElement(domElement); } function hydrateAttribute( diff --git a/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js b/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js index 64b4c0b0f7c98..49d1bae913933 100644 --- a/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js +++ b/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js @@ -58,42 +58,5 @@ describe('dangerouslySetInnerHTML', () => { innerHTMLDescriptor, ); }); - - // @gate !disableIEWorkarounds - it('sets innerHTML on it', async () => { - const html = ''; - const container = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg', - ); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - const circle = container.firstChild.firstChild; - expect(circle.tagName).toBe('circle'); - }); - - // @gate !disableIEWorkarounds - it('clears previous children', async () => { - const firstHtml = ''; - const secondHtml = ''; - - const container = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg', - ); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - const rect = container.firstChild.firstChild; - expect(rect.tagName).toBe('rect'); - await act(() => { - root.render(); - }); - const circle = container.firstChild.firstChild; - expect(circle.tagName).toBe('circle'); - }); }); }); diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index abe707badd5ee..de7198948c57c 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -231,29 +231,6 @@ describe('when Trusted Types are available in global object', () => { innerHTMLDescriptor, ); }); - - // @gate !disableIEWorkarounds - it('should log a warning', async () => { - class Component extends React.Component { - render() { - return ; - } - } - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - "Using 'dangerouslySetInnerHTML' in an svg element with " + - 'Trusted Types enabled in an Internet Explorer will cause ' + - 'the trusted value to be converted to string. Assigning string ' + - "to 'innerHTML' will throw an error if Trusted Types are enforced. " + - "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " + - 'on the enclosing div instead.', - ); - expect(container.innerHTML).toBe('unsafe html'); - }); }); it('should warn once when rendering script tag in jsx on client', async () => { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index f8aadb9789406..edaf4b2419136 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -203,10 +203,6 @@ export const disableLegacyContextForFunctionComponents = true; // TODO: clean up legacy once tests pass WWW. export const useModernStrictMode = true; -// Not ready to break experimental yet. -// Remove IE and MsApp specific workarounds for innerHTML -export const disableIEWorkarounds = true; - // Filter certain DOM attributes (e.g. src, href) if their values are empty // strings. This prevents e.g. from making an unnecessary HTTP // request for certain browsers. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 37991d4c8ffbd..1ecbcfe28f5ce 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -36,7 +36,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; export const disableDefaultPropsExceptForClasses = true; -export const disableIEWorkarounds = true; export const disableInputAttributeSyncing = false; export const disableLegacyContext = false; export const disableLegacyContextForFunctionComponents = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index eb40dc9123752..0d97b0b68d263 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -23,7 +23,6 @@ export const alwaysThrottleRetries = false; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; export const disableDefaultPropsExceptForClasses = true; -export const disableIEWorkarounds = true; export const disableInputAttributeSyncing = false; export const disableLegacyContext = true; export const disableLegacyContextForFunctionComponents = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 6673d056d9a09..28f96fb84e6bc 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -29,7 +29,6 @@ export const enablePostpone = false; export const enableHalt = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; -export const disableIEWorkarounds = true; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; export const enableSuspenseCallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 3ac86950277d4..fe3e2bd4813b8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -15,7 +15,6 @@ export const debugRenderPhaseSideEffectsForStrictMode = false; export const disableClientCache = true; export const disableCommentsAsDOMContainers = true; export const disableDefaultPropsExceptForClasses = true; -export const disableIEWorkarounds = true; export const disableInputAttributeSyncing = false; export const disableLegacyContext = false; export const disableLegacyContextForFunctionComponents = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 8ebb4d0dbf8db..1badea3abf309 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -29,7 +29,6 @@ export const enablePostpone = false; export const enableHalt = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; -export const disableIEWorkarounds = true; export const enableScopeAPI = true; export const enableCreateEventHandleAPI = false; export const enableSuspenseCallback = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 7a5761932c820..2a432305635d3 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -53,7 +53,6 @@ export const enableFabricCompleteRootInCommitPhase = false; export const enableSuspenseAvoidThisFallback = true; export const enableSuspenseAvoidThisFallbackFizz = false; -export const disableIEWorkarounds = true; export const enableCPUSuspense = true; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = true; diff --git a/scripts/jest/setupTests.www.js b/scripts/jest/setupTests.www.js index ba0ee287fd4bd..efee213861ca6 100644 --- a/scripts/jest/setupTests.www.js +++ b/scripts/jest/setupTests.www.js @@ -15,7 +15,6 @@ jest.mock('shared/ReactFeatureFlags', () => { // These are hardcoded to true for the next release, // but still run the tests against both variants until // we remove the flag. - actual.disableIEWorkarounds = __VARIANT__; actual.disableClientCache = __VARIANT__; return actual; From 5961888dd2ec23ef56ffcf8766e77322e72fe4cc Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 11:02:47 -0500 Subject: [PATCH 4/8] rm empty describe group --- .../__tests__/trustedTypes-test.internal.js | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index de7198948c57c..923ee1f5d81a6 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -206,33 +206,6 @@ describe('when Trusted Types are available in global object', () => { } }); - describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => { - let innerHTMLDescriptor; - - // simulate svg elements in Internet Explorer which don't have 'innerHTML' property - beforeEach(() => { - innerHTMLDescriptor = Object.getOwnPropertyDescriptor( - Element.prototype, - 'innerHTML', - ); - delete Element.prototype.innerHTML; - Object.defineProperty( - HTMLDivElement.prototype, - 'innerHTML', - innerHTMLDescriptor, - ); - }); - - afterEach(() => { - delete HTMLDivElement.prototype.innerHTML; - Object.defineProperty( - Element.prototype, - 'innerHTML', - innerHTMLDescriptor, - ); - }); - }); - it('should warn once when rendering script tag in jsx on client', async () => { const root = ReactDOMClient.createRoot(container); await expect(async () => { From 617e6d9b774474afea72db4657b5ac03cde6dfff Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 11:03:45 -0500 Subject: [PATCH 5/8] fix lint --- packages/react-dom-bindings/src/client/ReactDOMComponent.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index b62ad0bf62b74..ac95c91c596a8 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -16,7 +16,6 @@ import { possibleRegistrationNames, } from '../events/EventRegistry'; -import {canUseDOM} from 'shared/ExecutionEnvironment'; import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; @@ -50,7 +49,6 @@ import { } from './ReactDOMTextarea'; import {validateTextNesting} from './validateDOMNesting'; import {track} from './inputValueTracking'; -import setInnerHTML from './setInnerHTML'; import setTextContent from './setTextContent'; import { createDangerousStringForStyles, From 2076df723b713c4b81c616f7ebbb2e2d5f2b6a6c Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 11:38:04 -0500 Subject: [PATCH 6/8] rm another describe --- .../__tests__/dangerouslySetInnerHTML-test.js | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js b/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js index 49d1bae913933..ccbd41c2f30ad 100644 --- a/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js +++ b/packages/react-dom/src/client/__tests__/dangerouslySetInnerHTML-test.js @@ -27,36 +27,4 @@ describe('dangerouslySetInnerHTML', () => { expect(container.firstChild.innerHTML).toBe('

Hello

'); }); }); - - describe('when the node does not have an innerHTML property', () => { - let innerHTMLDescriptor; - - // In some versions of IE (TODO: which ones?) SVG nodes don't have - // innerHTML. To simulate this, we will take it off the Element prototype - // and put it onto the HTMLDivElement prototype. We expect that the logic - // checks for existence of innerHTML on SVG, and if one doesn't exist, falls - // back to using appendChild and removeChild. - - beforeEach(() => { - innerHTMLDescriptor = Object.getOwnPropertyDescriptor( - Element.prototype, - 'innerHTML', - ); - delete Element.prototype.innerHTML; - Object.defineProperty( - HTMLDivElement.prototype, - 'innerHTML', - innerHTMLDescriptor, - ); - }); - - afterEach(() => { - delete HTMLDivElement.prototype.innerHTML; - Object.defineProperty( - Element.prototype, - 'innerHTML', - innerHTMLDescriptor, - ); - }); - }); }); From 038fdfbe01c2cf5d7423d15864562d1309d80de7 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 10:52:31 -0500 Subject: [PATCH 7/8] Remove enableAsyncActions --- .../ReactHooksInspectionIntegration-test.js | 2 - .../src/client/ReactFiberConfigDOM.js | 6 +- .../src/shared/ReactDOMFormActions.js | 21 +- .../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/__tests__/ReactAsyncActions-test.js | 65 +- .../src/__tests__/ReactFlightDOMForm-test.js | 10 +- packages/react-server/src/ReactFizzHooks.js | 13 +- 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 - 24 files changed, 381 insertions(+), 663 deletions(-) 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..1ac16504cde93 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,9 @@ 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(); + // $FlowFixMe[not-a-function] We know this exists because of the feature check above. + return dispatcher.useHostTransitionStatus(); } export function useFormState( @@ -80,13 +75,9 @@ 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(); + // $FlowFixMe[not-a-function] This is unstable, thus optional + 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..01598c909f253 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 ( @@ -5206,12 +5106,42 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, + 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); + }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); mountHookTypesDev(); return mountId(); }, + 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/__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 78e114e373e6b..63dbe4e105ef2 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -42,7 +42,6 @@ import { enableCache, enableUseEffectEventHook, enableUseMemoCacheHook, - enableAsyncActions, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; import { @@ -851,6 +850,10 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useTransition: clientHookNotSupported, useId, useSyncExternalStore: clientHookNotSupported, + useOptimistic, + useActionState, + useFormState: useActionState, + useHostTransitionStatus, }; if (enableCache) { @@ -862,14 +865,6 @@ if (enableUseEffectEventHook) { if (enableUseMemoCacheHook) { HooksDispatcher.useMemoCache = useMemoCache; } -if (enableAsyncActions) { - HooksDispatcher.useHostTransitionStatus = useHostTransitionStatus; -} -if (enableAsyncActions) { - HooksDispatcher.useOptimistic = useOptimistic; - HooksDispatcher.useFormState = useActionState; - HooksDispatcher.useActionState = useActionState; -} export let currentResumableState: null | ResumableState = (null: any); export function setCurrentResumableState( diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 90b904bb42c75..18f4e4ee061fa 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, @@ -264,11 +261,7 @@ 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(); + // $FlowFixMe[not-a-function] This is unstable, thus optional + 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 c23745d6d6baaa9b48719ef2f41f3f4b529c12d9 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 12:54:48 -0500 Subject: [PATCH 8/8] Fixes and Dispatcher type --- .../react-debug-tools/src/ReactDebugHooks.js | 21 ++++--------------- .../src/shared/ReactDOMFormActions.js | 2 -- .../react-reconciler/src/ReactFiberHooks.js | 12 +++++------ .../src/ReactInternalTypes.js | 8 +++---- packages/react-server/src/ReactFizzHooks.js | 4 ++++ packages/react-server/src/ReactFlightHooks.js | 4 ++++ packages/react/src/ReactHooks.js | 2 -- 7 files changed, 22 insertions(+), 31 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-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js index 1ac16504cde93..6dd4e4da44479 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js @@ -66,7 +66,6 @@ function resolveDispatcher() { export function useFormStatus(): FormStatus { const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] We know this exists because of the feature check above. return dispatcher.useHostTransitionStatus(); } @@ -76,7 +75,6 @@ export function useFormState( permalink?: string, ): [Awaited, (P) => void, boolean] { const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.useFormState(action, initialState, permalink); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 01598c909f253..033e71610f5c6 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -5106,6 +5106,12 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountId(); + }, useFormState( action: (Awaited, P) => S, initialState: Awaited, @@ -5126,12 +5132,6 @@ if (__DEV__) { mountHookTypesDev(); return mountActionState(action, initialState, permalink); }, - useId(): string { - currentHookNameInDev = 'useId'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountId(); - }, useOptimistic( passthrough: S, reducer: ?(S, A) => S, 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-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 10227a9ceaa39..e4e81af8fabb8 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -832,6 +832,10 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs useId, // Subscriptions are not setup in a server environment. useSyncExternalStore, + useOptimistic, + useActionState, + useFormState: useActionState, + useHostTransitionStatus, } : { readContext, 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 18f4e4ee061fa..40c8a23d3798b 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -252,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); } @@ -262,6 +261,5 @@ export function useActionState( permalink?: string, ): [Awaited, (P) => void, boolean] { const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.useActionState(action, initialState, permalink); }