diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 4ca5ed10bc1..64a4b446ba5 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -116,6 +116,7 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {getIsStrictModeForDevtools} from './ReactFiberReconciler.new'; +import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1861,6 +1862,23 @@ function startTransition(setPending, callback) { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + if (__DEV__) { + if ( + prevTransition !== 1 && + warnOnSubscriptionInsideStartTransition && + ReactCurrentBatchConfig._updatedFibers + ) { + const updatedFibersCount = ReactCurrentBatchConfig._updatedFibers.size; + if (updatedFibersCount > 10) { + console.warn( + 'Detected a large number of updates inside startTransition. ' + + 'If this is due to a subscription please re-write it to use React provided hooks. ' + + 'Otherwise concurrent mode guarantees are off the table.', + ); + } + ReactCurrentBatchConfig._updatedFibers.clear(); + } + } } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index c87e1458f0b..11573e3b5e0 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -116,6 +116,7 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {getIsStrictModeForDevtools} from './ReactFiberReconciler.old'; +import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1861,6 +1862,23 @@ function startTransition(setPending, callback) { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + if (__DEV__) { + if ( + prevTransition !== 1 && + warnOnSubscriptionInsideStartTransition && + ReactCurrentBatchConfig._updatedFibers + ) { + const updatedFibersCount = ReactCurrentBatchConfig._updatedFibers.size; + if (updatedFibersCount > 10) { + console.warn( + 'Detected a large number of updates inside startTransition. ' + + 'If this is due to a subscription please re-write it to use React provided hooks. ' + + 'Otherwise concurrent mode guarantees are off the table.', + ); + } + ReactCurrentBatchConfig._updatedFibers.clear(); + } + } } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 7797448a82e..01b5f4e8190 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -30,6 +30,7 @@ import { enableStrictEffects, skipUnmountedBoundaries, enableUpdaterTracking, + warnOnSubscriptionInsideStartTransition, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -385,6 +386,13 @@ export function requestUpdateLane(fiber: Fiber): Lane { const isTransition = requestCurrentTransition() !== NoTransition; if (isTransition) { + if ( + __DEV__ && + warnOnSubscriptionInsideStartTransition && + ReactCurrentBatchConfig._updatedFibers + ) { + ReactCurrentBatchConfig._updatedFibers.add(fiber); + } // The algorithm for assigning an update to a lane should be stable for all // updates at the same priority within the same event. To do this, the // inputs to the algorithm must be the same. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 16369245623..e5ee9f4bd7b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -30,6 +30,7 @@ import { enableStrictEffects, skipUnmountedBoundaries, enableUpdaterTracking, + warnOnSubscriptionInsideStartTransition, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -385,6 +386,13 @@ export function requestUpdateLane(fiber: Fiber): Lane { const isTransition = requestCurrentTransition() !== NoTransition; if (isTransition) { + if ( + __DEV__ && + warnOnSubscriptionInsideStartTransition && + ReactCurrentBatchConfig._updatedFibers + ) { + ReactCurrentBatchConfig._updatedFibers.add(fiber); + } // The algorithm for assigning an update to a lane should be stable for all // updates at the same priority within the same event. To do this, the // inputs to the algorithm must be the same. diff --git a/packages/react/src/ReactCurrentBatchConfig.js b/packages/react/src/ReactCurrentBatchConfig.js index 9ba4a4c9c2c..cca41ae4b34 100644 --- a/packages/react/src/ReactCurrentBatchConfig.js +++ b/packages/react/src/ReactCurrentBatchConfig.js @@ -7,12 +7,22 @@ * @flow */ +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; + +type BatchConfig = { + transition: number, + _updatedFibers?: Set, +}; /** * Keeps track of the current batch's configuration such as how long an update * should suspend for if it needs to. */ -const ReactCurrentBatchConfig = { - transition: (0: number), +const ReactCurrentBatchConfig: BatchConfig = { + transition: 0, }; +if (__DEV__) { + ReactCurrentBatchConfig._updatedFibers = new Set(); +} + export default ReactCurrentBatchConfig; diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index afe1d2d282d..3f9c9efe714 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -8,6 +8,7 @@ */ import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; +import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; export function startTransition(scope: () => void) { const prevTransition = ReactCurrentBatchConfig.transition; @@ -16,5 +17,22 @@ export function startTransition(scope: () => void) { scope(); } finally { ReactCurrentBatchConfig.transition = prevTransition; + if (__DEV__) { + if ( + prevTransition !== 1 && + warnOnSubscriptionInsideStartTransition && + ReactCurrentBatchConfig._updatedFibers + ) { + const updatedFibersCount = ReactCurrentBatchConfig._updatedFibers.size; + if (updatedFibersCount > 10) { + console.warn( + 'Detected a large number of updates inside startTransition. ' + + 'If this is due to a subscription please re-write it to use React provided hooks. ' + + 'Otherwise concurrent mode guarantees are off the table.', + ); + } + ReactCurrentBatchConfig._updatedFibers.clear(); + } + } } } diff --git a/packages/react/src/__tests__/ReactStartTransition-test.js b/packages/react/src/__tests__/ReactStartTransition-test.js new file mode 100644 index 00000000000..94e45fb189e --- /dev/null +++ b/packages/react/src/__tests__/ReactStartTransition-test.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactTestRenderer; +let act; +let useState; +let useTransition; + +const SUSPICIOUS_NUMBER_OF_FIBERS_UPDATED = 10; + +describe('ReactStartTransition', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactTestRenderer = require('react-test-renderer'); + act = require('jest-react').act; + useState = React.useState; + useTransition = React.useTransition; + }); + + // @gate warnOnSubscriptionInsideStartTransition || !__DEV__ + it('Warns if a suspicious number of fibers are updated inside startTransition', () => { + const subs = new Set(); + const useUserSpaceSubscription = () => { + const setState = useState(0)[1]; + subs.add(setState); + }; + + let triggerHookTransition; + + const Component = ({level}) => { + useUserSpaceSubscription(); + if (level === 0) { + triggerHookTransition = useTransition()[1]; + } + if (level < SUSPICIOUS_NUMBER_OF_FIBERS_UPDATED) { + return ; + } + return null; + }; + + act(() => { + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + }); + + expect(() => { + act(() => { + React.startTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); + }); + }); + }); + }).toWarnDev( + [ + 'Detected a large number of updates inside startTransition. ' + + 'If this is due to a subscription please re-write it to use React provided hooks. ' + + 'Otherwise concurrent mode guarantees are off the table.', + ], + {withoutStack: true}, + ); + + expect(() => { + act(() => { + triggerHookTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); + }); + }); + }); + }).toWarnDev( + [ + 'Detected a large number of updates inside startTransition. ' + + 'If this is due to a subscription please re-write it to use React provided hooks. ' + + 'Otherwise concurrent mode guarantees are off the table.', + ], + {withoutStack: true}, + ); + }); +}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 618c32f1543..f6eb72375f8 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -99,6 +99,8 @@ export const enableTrustedTypesIntegration = false; // a deprecated pattern we want to get rid of in the future export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; + export const enableComponentStackLocations = true; export const enableNewReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 745b3d914dd..41122a6c5ff 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -48,6 +48,7 @@ export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index c3f9560c0b6..0bfe6f3fecc 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -39,6 +39,7 @@ export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index bd0d0cff3ee..c0b6eda1b03 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -39,6 +39,7 @@ export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index e1fdf781654..03d7ed0734d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -49,7 +49,7 @@ export const enableSuspenseLayoutEffectSemantics = false; export const enableGetInspectorDataForInstanceInProduction = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = false; - +export const warnOnSubscriptionInsideStartTransition = false; export const enableStrictEffects = false; export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 844d8665d3c..1618decf384 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -39,6 +39,7 @@ export const disableTextareaChildren = false; export const disableModulePatternComponents = true; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 09bb7152afc..cca9a0d7193 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -39,6 +39,7 @@ export const disableTextareaChildren = false; export const disableModulePatternComponents = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 1437c04ef07..e1b633538cd 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -39,6 +39,7 @@ export const disableTextareaChildren = __EXPERIMENTAL__; export const disableModulePatternComponents = true; export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; +export const warnOnSubscriptionInsideStartTransition = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = !__EXPERIMENTAL__; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 366f1fd0a70..d6a5c77b837 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -25,6 +25,7 @@ export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; export const enableLazyContextPropagation = __VARIANT__; export const enableSyncDefaultUpdates = __VARIANT__; export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__; +export const warnOnSubscriptionInsideStartTransition = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 3986cf94427..cd66a719143 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -31,6 +31,7 @@ export const { disableSchedulerTimeoutInWorkLoop, enableLazyContextPropagation, enableSyncDefaultUpdates, + warnOnSubscriptionInsideStartTransition, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. @@ -56,7 +57,6 @@ export const enableSchedulingProfiler = // For now, we'll turn it on for everyone because it's *already* on for everyone in practice. // At least this will let us stop shipping implementation to all users. export const enableSchedulerDebugging = true; - export const warnAboutDeprecatedLifecycles = true; export const disableLegacyContext = __EXPERIMENTAL__; export const warnAboutStringRefs = false;