From 379089d28875f565a488ec169e84c78e87a1dc4d Mon Sep 17 00:00:00 2001 From: Ricky Date: Wed, 8 Jan 2025 12:03:01 -0500 Subject: [PATCH 1/6] [flags] remove enableDeferRootSchedulingToMicrotask (#32008) Wait for me to merge, but this has landed everywhere and is ready to remove. --- .../src/ReactFiberRootScheduler.js | 14 +-- .../__tests__/ReactProfiler-test.internal.js | 107 +++++------------- 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 - .../forks/ReactFeatureFlags.www-dynamic.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 10 files changed, 32 insertions(+), 100 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index 4c4a35ac92c28..41daa6b806065 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -14,7 +14,6 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent'; import { disableLegacyMode, - enableDeferRootSchedulingToMicrotask, disableSchedulerTimeoutInWorkLoop, enableProfilerTimer, enableProfilerNestedUpdatePhase, @@ -139,14 +138,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void { } } - if (!enableDeferRootSchedulingToMicrotask) { - // While this flag is disabled, we schedule the render task immediately - // instead of waiting a microtask. - // TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to - // unblock additional features we have planned. - scheduleTaskForRootDuringMicrotask(root, now()); - } - if ( __DEV__ && !disableLegacyMode && @@ -325,10 +316,7 @@ function scheduleTaskForRootDuringMicrotask( // This function is always called inside a microtask, or at the very end of a // rendering task right before we yield to the main thread. It should never be // called synchronously. - // - // TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land - // that ASAP to unblock additional features we have planned. - // + // This function also never performs React work synchronously; it should // only schedule work to be performed later, in a separate task or microtask. diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index eb46f5b33a4ce..d51b037c18e4c 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -161,83 +161,38 @@ describe(`onRender`, () => { // Restore original mock jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); - // TODO: unstable_now is called by more places than just the profiler. - // Rewrite this test so it's less fragile. - if (gate(flags => flags.enableDeferRootSchedulingToMicrotask)) { - if (gate(flags => flags.enableComponentPerformanceTrack)) { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - } else { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - } + if (gate(flags => flags.enableComponentPerformanceTrack)) { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); } else { - if (gate(flags => flags.enableComponentPerformanceTrack)) { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - } else { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - } + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); } }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a8ab44136190e..1cb7a115c3a4f 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -37,10 +37,6 @@ export const favorSafetyOverHydrationPerf = true; // Need to remove didTimeout argument from Scheduler before landing export const disableSchedulerTimeoutInWorkLoop = false; -// This will break some internal tests at Meta so we need to gate this until -// those can be fixed. -export const enableDeferRootSchedulingToMicrotask = true; - // TODO: Land at Meta before removing. export const disableDefaultPropsExceptForClasses = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 7fd4fd7dfe69d..58a7d0891b014 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -45,7 +45,6 @@ export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; -export const enableDeferRootSchedulingToMicrotask = true; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableMoveBefore = true; export const enableFizzExternalRuntime = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 8a74a9e9dde9d..c24b45ff7f603 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -29,7 +29,6 @@ export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCPUSuspense = false; export const enableCreateEventHandleAPI = false; -export const enableDeferRootSchedulingToMicrotask = true; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFabricCompleteRootInCommitPhase = false; export const enableMoveBefore = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7156814474571..223c7e09a614f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -53,7 +53,6 @@ export const enableTransitionTracing = false; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFizzExternalRuntime = true; -export const enableDeferRootSchedulingToMicrotask = true; export const alwaysThrottleRetries = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 1777671e4eef1..c254872534186 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -24,7 +24,6 @@ export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; -export const enableDeferRootSchedulingToMicrotask = true; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableMoveBefore = false; export const enableFizzExternalRuntime = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index ab2dd11939382..fd2708b7b634e 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -55,7 +55,6 @@ export const enableTransitionTracing = false; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFizzExternalRuntime = false; -export const enableDeferRootSchedulingToMicrotask = true; export const alwaysThrottleRetries = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 69da420d0422c..6c99d5e278df4 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -17,7 +17,6 @@ export const alwaysThrottleRetries = true; export const disableDefaultPropsExceptForClasses = __VARIANT__; export const disableLegacyContextForFunctionComponents = __VARIANT__; export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; -export const enableDeferRootSchedulingToMicrotask = __VARIANT__; export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__; export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__; export const enableNoCloningMemoCache = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 6403d3592ba74..74f93e367fa3b 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -19,7 +19,6 @@ export const { disableDefaultPropsExceptForClasses, disableLegacyContextForFunctionComponents, disableSchedulerTimeoutInWorkLoop, - enableDeferRootSchedulingToMicrotask, enableDO_NOT_USE_disableStrictPassiveEffect, enableHiddenSubtreeInsertionEffectCleanup, enableInfiniteRenderLoopDetection, From e30c6693e4c7f2aec25b07f5df69a87163dbee81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 8 Jan 2025 12:08:30 -0500 Subject: [PATCH 2/6] [Fiber] Delete isMounted internals (#31966) The public API has been deleted a long time ago so this should be unused unless it's used by hacks. It should be replaced with an effect/lifecycle that manually tracks this if you need it. The problem with this API is how the timing implemented because it requires Placement/Hydration flags to be cleared too early. In fact, that's why we also have a separate PlacementDEV flag that works differently. https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberCommitWork.js#L2157-L2165 We should be able to remove this code now. --- .../__tests__/ReactComponentLifeCycle-test.js | 180 ------------------ .../src/ReactFiberClassComponent.js | 2 - .../react-reconciler/src/ReactFiberContext.js | 8 - .../src/ReactFiberTreeReflection.js | 35 ---- .../ReactIncrementalReflection-test.js | 88 --------- .../src/ReactFizzClassComponent.js | 3 - 6 files changed, 316 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index 75e4cebe80610..f985d06597673 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -12,7 +12,6 @@ let act; let React; -let ReactDOM; let ReactDOMClient; let assertConsoleErrorDev; let assertConsoleWarnDev; @@ -63,24 +62,6 @@ const POST_WILL_UNMOUNT_STATE = { hasWillUnmountCompleted: true, }; -/** - * Every React component is in one of these life cycles. - */ -type ComponentLifeCycle = - /** - * Mounted components have a DOM node representation and are capable of - * receiving new props. - */ - | 'MOUNTED' - /** - * Unmounted components are inactive and cannot receive new props. - */ - | 'UNMOUNTED'; - -function getLifeCycleState(instance): ComponentLifeCycle { - return instance.updater.isMounted(instance) ? 'MOUNTED' : 'UNMOUNTED'; -} - /** * TODO: We should make any setState calls fail in * `getInitialState` and `componentWillMount`. They will usually fail @@ -99,7 +80,6 @@ describe('ReactComponentLifeCycle', () => { } = require('internal-test-utils')); React = require('react'); - ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); }); @@ -290,137 +270,6 @@ describe('ReactComponentLifeCycle', () => { }); }); - it('should correctly determine if a component is mounted', async () => { - class Component extends React.Component { - _isMounted() { - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - return this.updater.isMounted(this); - } - UNSAFE_componentWillMount() { - expect(this._isMounted()).toBeFalsy(); - } - componentDidMount() { - expect(this._isMounted()).toBeTruthy(); - } - render() { - expect(this._isMounted()).toBeFalsy(); - return
; - } - } - - let instance; - const element = (instance = current)} />; - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(element); - }); - assertConsoleErrorDev([ - 'Component is accessing isMounted inside its render() function. ' + - 'render() should be a pure function of props and state. ' + - 'It should never access something that requires stale data ' + - 'from the previous render, such as refs. ' + - 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + - ' in Component (at **)', - ]); - expect(instance._isMounted()).toBeTruthy(); - }); - - it('should correctly determine if a null component is mounted', async () => { - class Component extends React.Component { - _isMounted() { - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - return this.updater.isMounted(this); - } - UNSAFE_componentWillMount() { - expect(this._isMounted()).toBeFalsy(); - } - componentDidMount() { - expect(this._isMounted()).toBeTruthy(); - } - render() { - expect(this._isMounted()).toBeFalsy(); - return null; - } - } - - let instance; - const element = (instance = current)} />; - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(element); - }); - assertConsoleErrorDev([ - 'Component is accessing isMounted inside its render() function. ' + - 'render() should be a pure function of props and state. ' + - 'It should never access something that requires stale data ' + - 'from the previous render, such as refs. ' + - 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + - ' in Component (at **)', - ]); - expect(instance._isMounted()).toBeTruthy(); - }); - - it('isMounted should return false when unmounted', async () => { - class Component extends React.Component { - render() { - return
; - } - } - - const root = ReactDOMClient.createRoot(document.createElement('div')); - const instanceRef = React.createRef(); - await act(() => { - root.render(); - }); - const instance = instanceRef.current; - - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - expect(instance.updater.isMounted(instance)).toBe(true); - - await act(() => { - root.unmount(); - }); - - expect(instance.updater.isMounted(instance)).toBe(false); - }); - - // @gate www && classic - it('warns if legacy findDOMNode is used inside render', async () => { - class Component extends React.Component { - state = {isMounted: false}; - componentDidMount() { - this.setState({isMounted: true}); - } - render() { - if (this.state.isMounted) { - expect(ReactDOM.findDOMNode(this).tagName).toBe('DIV'); - } - return
; - } - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - assertConsoleErrorDev([ - 'Component is accessing findDOMNode inside its render(). ' + - 'render() should be a pure function of props and state. ' + - 'It should never access something that requires stale data ' + - 'from the previous render, such as refs. ' + - 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + - ' in Component (at **)', - ]); - }); - it('should carry through each of the phases of setup', async () => { class LifeCycleComponent extends React.Component { constructor(props, context) { @@ -433,20 +282,16 @@ describe('ReactComponentLifeCycle', () => { hasWillUnmountCompleted: false, }; this._testJournal.returnedFromGetInitialState = clone(initState); - this._testJournal.lifeCycleAtStartOfGetInitialState = - getLifeCycleState(this); this.state = initState; } UNSAFE_componentWillMount() { this._testJournal.stateAtStartOfWillMount = clone(this.state); - this._testJournal.lifeCycleAtStartOfWillMount = getLifeCycleState(this); this.state.hasWillMountCompleted = true; } componentDidMount() { this._testJournal.stateAtStartOfDidMount = clone(this.state); - this._testJournal.lifeCycleAtStartOfDidMount = getLifeCycleState(this); this.setState({hasDidMountCompleted: true}); } @@ -454,10 +299,8 @@ describe('ReactComponentLifeCycle', () => { const isInitialRender = !this.state.hasRenderCompleted; if (isInitialRender) { this._testJournal.stateInInitialRender = clone(this.state); - this._testJournal.lifeCycleInInitialRender = getLifeCycleState(this); } else { this._testJournal.stateInLaterRender = clone(this.state); - this._testJournal.lifeCycleInLaterRender = getLifeCycleState(this); } // you would *NEVER* do anything like this in real code! this.state.hasRenderCompleted = true; @@ -466,8 +309,6 @@ describe('ReactComponentLifeCycle', () => { componentWillUnmount() { this._testJournal.stateAtStartOfWillUnmount = clone(this.state); - this._testJournal.lifeCycleAtStartOfWillUnmount = - getLifeCycleState(this); this.state.hasWillUnmountCompleted = true; } } @@ -480,52 +321,33 @@ describe('ReactComponentLifeCycle', () => { await act(() => { root.render(); }); - assertConsoleErrorDev([ - 'LifeCycleComponent is accessing isMounted inside its render() function. ' + - 'render() should be a pure function of props and state. ' + - 'It should never access something that requires stale data ' + - 'from the previous render, such as refs. ' + - 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + - ' in LifeCycleComponent (at **)', - ]); const instance = instanceRef.current; // getInitialState expect(instance._testJournal.returnedFromGetInitialState).toEqual( GET_INIT_STATE_RETURN_VAL, ); - expect(instance._testJournal.lifeCycleAtStartOfGetInitialState).toBe( - 'UNMOUNTED', - ); // componentWillMount expect(instance._testJournal.stateAtStartOfWillMount).toEqual( instance._testJournal.returnedFromGetInitialState, ); - expect(instance._testJournal.lifeCycleAtStartOfWillMount).toBe('UNMOUNTED'); // componentDidMount expect(instance._testJournal.stateAtStartOfDidMount).toEqual( DID_MOUNT_STATE, ); - expect(instance._testJournal.lifeCycleAtStartOfDidMount).toBe('MOUNTED'); // initial render expect(instance._testJournal.stateInInitialRender).toEqual( INIT_RENDER_STATE, ); - expect(instance._testJournal.lifeCycleInInitialRender).toBe('UNMOUNTED'); - - expect(getLifeCycleState(instance)).toBe('MOUNTED'); // Now *update the component* instance.forceUpdate(); // render 2nd time expect(instance._testJournal.stateInLaterRender).toEqual(NEXT_RENDER_STATE); - expect(instance._testJournal.lifeCycleInLaterRender).toBe('MOUNTED'); - - expect(getLifeCycleState(instance)).toBe('MOUNTED'); await act(() => { root.unmount(); @@ -535,10 +357,8 @@ describe('ReactComponentLifeCycle', () => { WILL_UNMOUNT_STATE, ); // componentWillUnmount called right before unmount. - expect(instance._testJournal.lifeCycleAtStartOfWillUnmount).toBe('MOUNTED'); // But the current lifecycle of the component is unmounted. - expect(getLifeCycleState(instance)).toBe('UNMOUNTED'); expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE); }); diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index d0490b7e02d4e..a5d7ef12ab6ce 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -23,7 +23,6 @@ import { disableDefaultPropsExceptForClasses, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; -import {isMounted} from './ReactFiberTreeReflection'; import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap'; import shallowEqual from 'shared/shallowEqual'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -165,7 +164,6 @@ function applyDerivedStateFromProps( } const classComponentUpdater = { - isMounted, // $FlowFixMe[missing-local-annot] enqueueSetState(inst: any, payload: any, callback) { const fiber = getInstance(inst); diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 93e9eec922f73..e8a586e856e1d 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -10,7 +10,6 @@ import type {Fiber} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack'; -import {isFiberMounted} from './ReactFiberTreeReflection'; import {disableLegacyContext} from 'shared/ReactFeatureFlags'; import {ClassComponent, HostRoot} from './ReactWorkTags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -285,13 +284,6 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { } else { // Currently this is only used with renderSubtreeIntoContainer; not sure if it // makes sense elsewhere - if (!isFiberMounted(fiber) || fiber.tag !== ClassComponent) { - throw new Error( - 'Expected subtree parent to be a mounted class component. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - } - let node: Fiber = fiber; do { switch (node.tag) { diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index e0199108693e3..5dce60a559fd8 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -11,10 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Container, SuspenseInstance} from './ReactFiberConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; -import {get as getInstance} from 'shared/ReactInstanceMap'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import { - ClassComponent, HostComponent, HostHoistable, HostSingleton, @@ -24,7 +21,6 @@ import { SuspenseComponent, } from './ReactWorkTags'; import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; -import {current as currentOwner, isRendering} from './ReactCurrentFiber'; export function getNearestMountedFiber(fiber: Fiber): null | Fiber { let node = fiber; @@ -83,37 +79,6 @@ export function getContainerFromFiber(fiber: Fiber): null | Container { : null; } -export function isFiberMounted(fiber: Fiber): boolean { - return getNearestMountedFiber(fiber) === fiber; -} - -export function isMounted(component: React$Component): boolean { - if (__DEV__) { - const owner = currentOwner; - if (owner !== null && isRendering && owner.tag === ClassComponent) { - const ownerFiber: Fiber = owner; - const instance = ownerFiber.stateNode; - if (!instance._warnedAboutRefsInRender) { - console.error( - '%s is accessing isMounted inside its render() function. ' + - 'render() should be a pure function of props and state. It should ' + - 'never access something that requires stale data from the previous ' + - 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', - getComponentNameFromFiber(ownerFiber) || 'A component', - ); - } - instance._warnedAboutRefsInRender = true; - } - } - - const fiber: ?Fiber = getInstance(component); - if (!fiber) { - return false; - } - return getNearestMountedFiber(fiber) === fiber; -} - function assertIsMounted(fiber: Fiber) { if (getNearestMountedFiber(fiber) !== fiber) { throw new Error('Unable to find node on an unmounted component.'); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js index cffd690e64715..63ff106bf8432 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js @@ -40,94 +40,6 @@ describe('ReactIncrementalReflection', () => { return {type: 'span', children: [], prop, hidden: false}; } - it('handles isMounted even when the initial render is deferred', async () => { - const instances = []; - - class Component extends React.Component { - _isMounted() { - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - return this.updater.isMounted(this); - } - UNSAFE_componentWillMount() { - instances.push(this); - Scheduler.log('componentWillMount: ' + this._isMounted()); - } - componentDidMount() { - Scheduler.log('componentDidMount: ' + this._isMounted()); - } - render() { - return ; - } - } - - function Foo() { - return ; - } - - React.startTransition(() => { - ReactNoop.render(); - }); - - // Render part way through but don't yet commit the updates. - await waitFor(['componentWillMount: false']); - - expect(instances[0]._isMounted()).toBe(false); - - // Render the rest and commit the updates. - await waitForAll(['componentDidMount: true']); - - expect(instances[0]._isMounted()).toBe(true); - }); - - it('handles isMounted when an unmount is deferred', async () => { - const instances = []; - - class Component extends React.Component { - _isMounted() { - return this.updater.isMounted(this); - } - UNSAFE_componentWillMount() { - instances.push(this); - } - componentWillUnmount() { - Scheduler.log('componentWillUnmount: ' + this._isMounted()); - } - render() { - Scheduler.log('Component'); - return ; - } - } - - function Other() { - Scheduler.log('Other'); - return ; - } - - function Foo(props) { - return props.mount ? : ; - } - - ReactNoop.render(); - await waitForAll(['Component']); - - expect(instances[0]._isMounted()).toBe(true); - - React.startTransition(() => { - ReactNoop.render(); - }); - // Render part way through but don't yet commit the updates so it is not - // fully unmounted yet. - await waitFor(['Other']); - - expect(instances[0]._isMounted()).toBe(true); - - // Finish flushing the unmount. - await waitForAll(['componentWillUnmount: true']); - - expect(instances[0]._isMounted()).toBe(false); - }); - it('finds no node before insertion and correct node before deletion', async () => { let classInstance = null; diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js index 08b4aaa94c79b..8106b672d64d2 100644 --- a/packages/react-server/src/ReactFizzClassComponent.js +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -108,9 +108,6 @@ type InternalInstance = { }; const classComponentUpdater = { - isMounted(inst: any) { - return false; - }, // $FlowFixMe[missing-local-annot] enqueueSetState(inst: any, payload: any, callback) { const internals: InternalInstance = getInstance(inst); From a4d122f2d192fe0b6480e669cca43c8f953aaf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 8 Jan 2025 12:11:18 -0500 Subject: [PATCH 3/6] Add Component (#31975) This will provide the opt-in for using [View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) in React. View Transitions only trigger for async updates like `startTransition`, `useDeferredValue`, Actions or `` revealing from fallback to content. Synchronous updates provide an opt-out but also guarantee that they commit immediately which View Transitions can't. There's no need to opt-in to View Transitions at the "cause" side like event handlers or actions. They don't know what UI will change and whether that has an animated transition described. Conceptually the `` component is like a DOM fragment that transitions its children in its own isolate/snapshot. The API works by wrapping a DOM node or inner component: ```js import {ViewTransition} from 'react'; ``` The default is `name="auto"` which will automatically assign a `view-transition-name` to the inner DOM node. That way you can add a View Transition to a Component without controlling its DOM nodes styling otherwise. A difference between this and the browser's built-in `view-transition-name: auto` is that switching the DOM nodes within the `` component preserves the same name so this example cross-fades between the DOM nodes instead of causing an exit and enter: ```js {condition ? : } ``` This becomes especially useful with `` as this example cross-fades between Skeleton and Content: ```js }> ``` Where as this example triggers an exit of the Skeleton and an enter of the Content: ```js }> ``` Managing instances and keys becomes extra important. You can also specify an explicit `name` property for example for animating the same conceptual item from one page onto another. However, best practices is to property namespace these since they can easily collide. It's also useful to add an `id` to it if available. ```js ``` The model in general is the same as plain `view-transition-name` except React manages a set of heuristics for when to apply it. A problem with the naive View Transitions model is that it overly opts in every boundary that *might* transition into transitioning. This is leads to unfortunate effects like things floating around when unrelated updates happen. This leads the whole document to animate which means that nothing is clickable in the meantime. It makes it not useful for smaller and more local transitions. Best practice is to add `view-transition-name` only right before you're about to need to animate the thing. This is tricky to manage globally on complex apps and is not compositional. Instead we let React manage when a `` "activates" and add/remove the `view-transition-name`. This is also when React calls `startViewTransition` behind the scenes while it mutates the DOM. I've come up with a number of heuristics that I think will make a lot easier to coordinate this. The principle is that only if something that updates that particular boundary do we activate it. I hope that one day maybe browsers will have something like these built-in and we can remove our implementation. A `` only activates if: - If a mounted Component renders a `` within it outside the first DOM node, and it is within the viewport, then that ViewTransition activates as an "enter" animation. This avoids inner "enter" animations trigger when the parent mounts. - If an unmounted Component had a `` within it outside the first DOM node, and it was within the viewport, then that ViewTransition activates as an "exit" animation. This avoids inner "exit" animations triggering when the parent unmounts. - If an explicitly named `` is deep within an unmounted tree and one with the same name appears in a mounted tree at the same time, then both are activated as a pair, but only if they're both in the viewport. This avoids these triggering "enter" or "exit" animations when going between parents that don't have a pair. - If an already mounted `` is visible and a DOM mutation, that might affect how it's painted, happens within its children but outside any nested ``. This allows it to "cross-fade" between its updates. - If an already mounted `` resizes or moves as the result of direct DOM nodes siblings changing or moving around. This allows insertion, deletion and reorders into a list to animate all children. It is only within one DOM node though, to avoid unrelated changes in the parent to trigger this. If an item is outside the viewport before and after, then it's skipped to avoid things flying across the screen. - If a `` boundary changes size, due to a DOM mutation within it, then the parent activates (or the root document if there are no more parents). This ensures that the container can cross-fade to avoid abrupt relayout. This can be avoided by using absolutely positioned children. When this can avoid bubbling to the root document, whatever is not animating is still responsive to clicks during the transition. Conceptually each DOM node has its own default that activates the parent `` or no transition if the parent is the root. That means that if you add a DOM node like `
` this won't trigger an "enter" animation since it was the div that was added, not the ViewTransition. Instead, it might cause a cross-fade of the parent ViewTransition or no transition if it had no parent. This ensures that only explicit boundaries perform coarse animations instead of every single node which is really the benefit of the View Transitions model. This ends up working out well for simple cases like switching between two pages immediately while transitioning one floating item that appears on both pages. Because only the floating item transitions by default. Note that it's possible to add manual `view-transition-name` with CSS or `style={{ viewTransitionName: 'auto' }}` that always transitions as long as something else has a `` that activates. For example a `` can wrap a whole page for a cross-fade but inside of it an explicit name can be added to something to ensure it animates as a move when something relates else changes its layout. Instead of just cross-fading it along with the Page which would be the default. There's more PRs coming with some optimizations, fixes and expanded APIs. This first PR explores the above core heuristic. --------- Co-authored-by: Sebastian "Sebbie" Silbermann --- fixtures/view-transition/README.md | 30 + fixtures/view-transition/package.json | 28 + fixtures/view-transition/public/favicon.ico | Bin 0 -> 24838 bytes fixtures/view-transition/public/index.html | 13 + fixtures/view-transition/server/index.js | 70 + fixtures/view-transition/server/render.js | 44 + .../view-transition/src/components/App.js | 12 + .../view-transition/src/components/Chrome.css | 5 + .../view-transition/src/components/Chrome.js | 33 + .../view-transition/src/components/Page.css | 0 .../view-transition/src/components/Page.js | 79 + fixtures/view-transition/src/index.js | 6 + fixtures/view-transition/yarn.lock | 7049 +++++++++++++++++ packages/react-art/src/ReactFiberConfigART.js | 45 + packages/react-devtools-shared/src/utils.js | 4 + .../src/client/CSSPropertyOperations.js | 3 + .../src/client/DOMPropertyOperations.js | 3 + .../src/client/ReactDOMComponent.js | 76 +- .../src/client/ReactFiberConfigDOM.js | 226 +- packages/react-is/src/ReactIs.js | 2 + .../src/ReactFiberConfigNative.js | 67 + .../src/createReactNoop.js | 47 + packages/react-reconciler/src/ReactFiber.js | 29 + .../src/ReactFiberBeginWork.js | 44 + .../src/ReactFiberCommitHostEffects.js | 11 +- .../src/ReactFiberCommitWork.js | 1094 ++- .../src/ReactFiberCompleteWork.js | 75 + .../src/ReactFiberConfigWithNoMutation.js | 11 + .../react-reconciler/src/ReactFiberFlags.js | 87 +- .../react-reconciler/src/ReactFiberLane.js | 4 + .../src/ReactFiberMutationTracking.js | 33 + .../src/ReactFiberViewTransitionComponent.js | 70 + .../src/ReactFiberWorkLoop.js | 107 +- .../react-reconciler/src/ReactWorkTags.js | 4 +- .../src/forks/ReactFiberConfig.custom.js | 13 + .../src/ReactFizzComponentStack.js | 10 +- packages/react-server/src/ReactFizzServer.js | 12 + .../src/ReactFiberConfigTestHost.js | 66 + .../react/index.experimental.development.js | 1 + packages/react/index.experimental.js | 1 + packages/react/index.js | 1 + packages/react/src/ReactClient.js | 3 + .../ReactServer.experimental.development.js | 6 +- .../react/src/ReactServer.experimental.js | 6 +- packages/shared/ReactComponentStackFrame.js | 7 + packages/shared/ReactFeatureFlags.js | 2 + packages/shared/ReactSerializationErrors.js | 7 + packages/shared/ReactSymbols.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 + packages/shared/getComponentNameFromType.js | 7 + packages/shared/isValidElementType.js | 5 +- scripts/error-codes/codes.json | 3 +- 57 files changed, 9473 insertions(+), 98 deletions(-) create mode 100644 fixtures/view-transition/README.md create mode 100644 fixtures/view-transition/package.json create mode 100644 fixtures/view-transition/public/favicon.ico create mode 100644 fixtures/view-transition/public/index.html create mode 100644 fixtures/view-transition/server/index.js create mode 100644 fixtures/view-transition/server/render.js create mode 100644 fixtures/view-transition/src/components/App.js create mode 100644 fixtures/view-transition/src/components/Chrome.css create mode 100644 fixtures/view-transition/src/components/Chrome.js create mode 100644 fixtures/view-transition/src/components/Page.css create mode 100644 fixtures/view-transition/src/components/Page.js create mode 100644 fixtures/view-transition/src/index.js create mode 100644 fixtures/view-transition/yarn.lock create mode 100644 packages/react-reconciler/src/ReactFiberMutationTracking.js create mode 100644 packages/react-reconciler/src/ReactFiberViewTransitionComponent.js diff --git a/fixtures/view-transition/README.md b/fixtures/view-transition/README.md new file mode 100644 index 0000000000000..7f5642ee9c4c3 --- /dev/null +++ b/fixtures/view-transition/README.md @@ -0,0 +1,30 @@ +# View Transition + +A test case for View Transitions. + +## Setup + +To reference a local build of React, first run `npm run build` at the root +of the React project. Then: + +``` +cd fixtures/view-transition +yarn +yarn start +``` + +The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading. + +**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:** + +``` +yarn +``` + +If you want to try the production mode instead run: + +``` +yarn start:prod +``` + +This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading). diff --git a/fixtures/view-transition/package.json b/fixtures/view-transition/package.json new file mode 100644 index 0000000000000..8d47a5b17dcec --- /dev/null +++ b/fixtures/view-transition/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-fixtures-view-transition", + "version": "0.1.0", + "private": true, + "devDependencies": { + "concurrently": "3.1.0", + "http-proxy-middleware": "0.17.3", + "react-scripts": "0.9.5" + }, + "dependencies": { + "express": "^4.14.0", + "ignore-styles": "^5.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "scripts": { + "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", + "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/", + "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", + "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", + "dev:client": "PORT=3001 react-scripts start", + "dev:server": "NODE_ENV=development node server", + "start": "react-scripts build && NODE_ENV=production node server", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/fixtures/view-transition/public/favicon.ico b/fixtures/view-transition/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5c125de5d897c1ff5692a656485b3216123dcd89 GIT binary patch literal 24838 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 + + + + + diff --git a/fixtures/view-transition/server/index.js b/fixtures/view-transition/server/index.js new file mode 100644 index 0000000000000..dd8a783b50842 --- /dev/null +++ b/fixtures/view-transition/server/index.js @@ -0,0 +1,70 @@ +require('ignore-styles'); +const babelRegister = require('babel-register'); +const proxy = require('http-proxy-middleware'); + +babelRegister({ + ignore: /\/(build|node_modules)\//, + presets: ['react-app'], +}); + +const express = require('express'); +const path = require('path'); + +const app = express(); + +// Application +if (process.env.NODE_ENV === 'development') { + app.get('/', function (req, res) { + // In development mode we clear the module cache between each request to + // get automatic hot reloading. + for (var key in require.cache) { + delete require.cache[key]; + } + const render = require('./render').default; + render(req.url, res); + }); +} else { + const render = require('./render').default; + app.get('/', function (req, res) { + render(req.url, res); + }); +} + +// Static resources +app.use(express.static(path.resolve(__dirname, '..', 'build'))); + +// Proxy everything else to create-react-app's webpack development server +if (process.env.NODE_ENV === 'development') { + app.use( + '/', + proxy({ + ws: true, + target: 'http://localhost:3001', + }) + ); +} + +app.listen(3000, () => { + console.log('Listening on port 3000...'); +}); + +app.on('error', function (error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}); diff --git a/fixtures/view-transition/server/render.js b/fixtures/view-transition/server/render.js new file mode 100644 index 0000000000000..1b37987edb428 --- /dev/null +++ b/fixtures/view-transition/server/render.js @@ -0,0 +1,44 @@ +import React from 'react'; +import {renderToPipeableStream} from 'react-dom/server'; + +import App from '../src/components/App'; + +let assets; +if (process.env.NODE_ENV === 'development') { + // Use the bundle from create-react-app's server in development mode. + assets = { + 'main.js': '/static/js/bundle.js', + // 'main.css': '', + }; +} else { + assets = require('../build/asset-manifest.json'); +} + +export default function render(url, res) { + res.socket.on('error', error => { + // Log fatal errors + console.error('Fatal', error); + }); + let didError = false; + const {pipe, abort} = renderToPipeableStream(, { + bootstrapScripts: [assets['main.js']], + onShellReady() { + // If something errored before we started streaming, we set the error code appropriately. + res.statusCode = didError ? 500 : 200; + res.setHeader('Content-type', 'text/html'); + pipe(res); + }, + onShellError(x) { + // Something errored before we could complete the shell so we emit an alternative shell. + res.statusCode = 500; + res.send('

Error

'); + }, + onError(x) { + didError = true; + console.error(x); + }, + }); + // Abandon and switch to client rendering after 5 seconds. + // Try lowering this to see the client recover. + setTimeout(abort, 5000); +} diff --git a/fixtures/view-transition/src/components/App.js b/fixtures/view-transition/src/components/App.js new file mode 100644 index 0000000000000..6867b29d4c5a5 --- /dev/null +++ b/fixtures/view-transition/src/components/App.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import Chrome from './Chrome'; +import Page from './Page'; + +export default function App({assets}) { + return ( + + + + ); +} diff --git a/fixtures/view-transition/src/components/Chrome.css b/fixtures/view-transition/src/components/Chrome.css new file mode 100644 index 0000000000000..b019b57b1db81 --- /dev/null +++ b/fixtures/view-transition/src/components/Chrome.css @@ -0,0 +1,5 @@ +body { + margin: 10px; + padding: 0; + font-family: sans-serif; +} diff --git a/fixtures/view-transition/src/components/Chrome.js b/fixtures/view-transition/src/components/Chrome.js new file mode 100644 index 0000000000000..0cae4a8dd1953 --- /dev/null +++ b/fixtures/view-transition/src/components/Chrome.js @@ -0,0 +1,33 @@ +import React, {Component} from 'react'; + +import './Chrome.css'; + +export default class Chrome extends Component { + render() { + const assets = this.props.assets; + return ( + + + + + + + {this.props.title} + + +