diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 4cc4dda6fa611..0a9f89d13b9c3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -119,6 +119,7 @@ import { } from './ReactFiberConcurrentUpdates.new'; import {getTreeId} from './ReactFiberTreeContext.new'; import {now} from './Scheduler'; +import {enableThrowOnMountForHookMismatch} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -241,11 +242,18 @@ function updateHookTypesDev() { if (__DEV__) { const hookName = ((currentHookNameInDev: any): HookType); + hookTypesUpdateIndexDev++; if (hookTypesDev !== null) { - hookTypesUpdateIndexDev++; if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) { - warnOnHookMismatchInDev(hookName); + warnOnHookMismatchInDev( + hookName, + hookTypesUpdateIndexDev, + hookTypesDev, + ); } + } else { + // Going from no hooks -> some hooks. + warnOnHookMismatchInDev(hookName, hookTypesUpdateIndexDev, []); } } } @@ -265,22 +273,45 @@ function checkDepsAreArrayDev(deps: mixed) { } } -function warnOnHookMismatchInDev(currentHookName: HookType) { +function warnOnHookMismatchInDev( + currentHookName: ?HookType, + currentHookIndex: number, + expectedHookNames: Array, +) { if (__DEV__) { const componentName = getComponentNameFromFiber(currentlyRenderingFiber); if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) { didWarnAboutMismatchedHooksForComponent.add(componentName); - if (hookTypesDev !== null) { - let table = ''; + let table = ''; + + const secondColumnStart = 30; + + if (currentHookIndex === -1 && expectedHookNames.length > 0) { + // When going from some hooks to no hooks, currentHookIndex === -1, + // so we need to print all of the expected hooks going to undefined. + for (let i = 0; i <= expectedHookNames.length - 1; i++) { + const oldHookName = expectedHookNames[i]; + const newHookName = 'undefined'; + + let row = `${i + 1}. ${oldHookName}`; + + // Extra space so second column lines up + // lol @ IE not supporting String#repeat + while (row.length < secondColumnStart) { + row += ' '; + } - const secondColumnStart = 30; + row += newHookName + '\n'; - for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) { - const oldHookName = hookTypesDev[i]; + table += row; + } + } else { + for (let i = 0; i <= currentHookIndex; i++) { + const oldHookName = expectedHookNames[i]; const newHookName = i === ((hookTypesUpdateIndexDev: any): number) - ? currentHookName + ? currentHookName || 'undefined' : oldHookName; let row = `${i + 1}. ${oldHookName}`; @@ -295,19 +326,19 @@ function warnOnHookMismatchInDev(currentHookName: HookType) { table += row; } - - console.error( - 'React has detected a change in the order of Hooks called by %s. ' + - 'This will lead to bugs and errors if not fixed. ' + - 'For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' ------------------------------------------------------\n' + - '%s' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', - componentName, - table, - ); } + + console.error( + 'React has detected a change in the order of Hooks called by %s. ' + + 'This will lead to bugs and errors if not fixed. ' + + 'For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '%s' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', + componentName, + table, + ); } } } @@ -383,9 +414,8 @@ export function renderWithHooks( if (__DEV__) { hookTypesDev = - current !== null - ? ((current._debugHookTypes: any): Array) - : null; + current !== null ? ((current._debugHookTypes: any): Array) : []; + hookTypesUpdateIndexDev = -1; // Used for hot reloading: ignorePreviousDependencies = @@ -403,31 +433,44 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; - // TODO Warn if no hooks are used at all during mount, then some are used during update. - // Currently we will identify the update render as a mount because memoizedState === null. - // This is tricky because it's valid for certain types of components (e.g. React.lazy) - - // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. - // Non-stateful hooks (e.g. context) don't get added to memoizedState, - // so memoizedState would be null during updates and mounts. if (__DEV__) { - if (current !== null && current.memoizedState !== null) { - ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; - } else if (hookTypesDev !== null) { - // This dispatcher handles an edge case where a component is updating, - // but no stateful hooks have been used. - // We want to match the production code behavior (which will use HooksDispatcherOnMount), - // but with the extra DEV validation to ensure hooks ordering hasn't changed. - // This dispatcher does that. - ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + if (current !== null) { + if (enableThrowOnMountForHookMismatch && current.mode & ConcurrentMode) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else { + // In Legacy Mode, memoizedState is sometimes null even during the + // initial mount because of an edge case with React.lazy. Hence the + // excessively convoluted logic in this branch. + // + // TODO: Delete this branch + // + // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. + // Non-stateful hooks (e.g. context) don't get added to memoizedState, + // so memoizedState would be null during updates and mounts. + if (current.memoizedState !== null) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else if (hookTypesDev !== null) { + // This dispatcher handles an edge case where a component is updating, + // but no stateful hooks have been used. + // We want to match the production code behavior (which will use HooksDispatcherOnMount), + // but with the extra DEV validation to ensure hooks ordering hasn't changed. + // This dispatcher does that. + ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + } else { + ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; + } + } } else { ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; } } else { ReactCurrentDispatcher.current = - current === null || current.memoizedState === null - ? HooksDispatcherOnMount - : HooksDispatcherOnUpdate; + current !== null && + ((enableThrowOnMountForHookMismatch && + (current.mode & ConcurrentMode) !== NoMode) || + current.memoizedState !== null) + ? HooksDispatcherOnUpdate + : HooksDispatcherOnMount; } let children = Component(props, secondArg); @@ -479,11 +522,25 @@ export function renderWithHooks( ReactCurrentDispatcher.current = ContextOnlyDispatcher; if (__DEV__) { + if ( + current !== null && + (current.mode & ConcurrentMode || current.memoizedState !== null) && + hookTypesDev != null && + hookTypesDev.length > 0 && + hookTypesUpdateIndexDev === -1 + ) { + // If we get here and have hookTypesDev, but the update index hasn't incremented, + // then none of the previously mounted hooks have been called, and we need to warn. + warnOnHookMismatchInDev(undefined, hookTypesUpdateIndexDev, hookTypesDev); + } + workInProgress._debugHookTypes = hookTypesDev; } // This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. + // This doesn't catch going from some hooks to no hooks, but that's not worth + // the additional check right now. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 81fdcb7696be1..34ba63d8eb83f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -117,6 +117,7 @@ import { } from './ReactFiberConcurrentUpdates.old'; import {getTreeId} from './ReactFiberTreeContext.old'; import {now} from './Scheduler'; +import {enableThrowOnMountForHookMismatch} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -240,11 +241,18 @@ function updateHookTypesDev() { if (__DEV__) { const hookName = ((currentHookNameInDev: any): HookType); + hookTypesUpdateIndexDev++; if (hookTypesDev !== null) { - hookTypesUpdateIndexDev++; if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) { - warnOnHookMismatchInDev(hookName); + warnOnHookMismatchInDev( + hookName, + hookTypesUpdateIndexDev, + hookTypesDev, + ); } + } else { + // Going from no hooks -> some hooks. + warnOnHookMismatchInDev(hookName, hookTypesUpdateIndexDev, []); } } } @@ -264,22 +272,45 @@ function checkDepsAreArrayDev(deps: mixed) { } } -function warnOnHookMismatchInDev(currentHookName: HookType) { +function warnOnHookMismatchInDev( + currentHookName: ?HookType, + currentHookIndex: number, + expectedHookNames: Array, +) { if (__DEV__) { const componentName = getComponentNameFromFiber(currentlyRenderingFiber); if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) { didWarnAboutMismatchedHooksForComponent.add(componentName); - if (hookTypesDev !== null) { - let table = ''; + let table = ''; + + const secondColumnStart = 30; + + if (currentHookIndex === -1 && expectedHookNames.length > 0) { + // When going from some hooks to no hooks, currentHookIndex === -1, + // so we need to print all of the expected hooks going to undefined. + for (let i = 0; i <= expectedHookNames.length - 1; i++) { + const oldHookName = expectedHookNames[i]; + const newHookName = 'undefined'; + + let row = `${i + 1}. ${oldHookName}`; + + // Extra space so second column lines up + // lol @ IE not supporting String#repeat + while (row.length < secondColumnStart) { + row += ' '; + } - const secondColumnStart = 30; + row += newHookName + '\n'; - for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) { - const oldHookName = hookTypesDev[i]; + table += row; + } + } else { + for (let i = 0; i <= currentHookIndex; i++) { + const oldHookName = expectedHookNames[i]; const newHookName = i === ((hookTypesUpdateIndexDev: any): number) - ? currentHookName + ? currentHookName || 'undefined' : oldHookName; let row = `${i + 1}. ${oldHookName}`; @@ -294,19 +325,19 @@ function warnOnHookMismatchInDev(currentHookName: HookType) { table += row; } - - console.error( - 'React has detected a change in the order of Hooks called by %s. ' + - 'This will lead to bugs and errors if not fixed. ' + - 'For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' ------------------------------------------------------\n' + - '%s' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', - componentName, - table, - ); } + + console.error( + 'React has detected a change in the order of Hooks called by %s. ' + + 'This will lead to bugs and errors if not fixed. ' + + 'For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '%s' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', + componentName, + table, + ); } } } @@ -382,9 +413,8 @@ export function renderWithHooks( if (__DEV__) { hookTypesDev = - current !== null - ? ((current._debugHookTypes: any): Array) - : null; + current !== null ? ((current._debugHookTypes: any): Array) : []; + hookTypesUpdateIndexDev = -1; // Used for hot reloading: ignorePreviousDependencies = @@ -402,31 +432,44 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; - // TODO Warn if no hooks are used at all during mount, then some are used during update. - // Currently we will identify the update render as a mount because memoizedState === null. - // This is tricky because it's valid for certain types of components (e.g. React.lazy) - - // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. - // Non-stateful hooks (e.g. context) don't get added to memoizedState, - // so memoizedState would be null during updates and mounts. if (__DEV__) { - if (current !== null && current.memoizedState !== null) { - ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; - } else if (hookTypesDev !== null) { - // This dispatcher handles an edge case where a component is updating, - // but no stateful hooks have been used. - // We want to match the production code behavior (which will use HooksDispatcherOnMount), - // but with the extra DEV validation to ensure hooks ordering hasn't changed. - // This dispatcher does that. - ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + if (current !== null) { + if (enableThrowOnMountForHookMismatch && current.mode & ConcurrentMode) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else { + // In Legacy Mode, memoizedState is sometimes null even during the + // initial mount because of an edge case with React.lazy. Hence the + // excessively convoluted logic in this branch. + // + // TODO: Delete this branch + // + // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. + // Non-stateful hooks (e.g. context) don't get added to memoizedState, + // so memoizedState would be null during updates and mounts. + if (current.memoizedState !== null) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else if (hookTypesDev !== null) { + // This dispatcher handles an edge case where a component is updating, + // but no stateful hooks have been used. + // We want to match the production code behavior (which will use HooksDispatcherOnMount), + // but with the extra DEV validation to ensure hooks ordering hasn't changed. + // This dispatcher does that. + ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + } else { + ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; + } + } } else { ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; } } else { ReactCurrentDispatcher.current = - current === null || current.memoizedState === null - ? HooksDispatcherOnMount - : HooksDispatcherOnUpdate; + current !== null && + ((enableThrowOnMountForHookMismatch && + (current.mode & ConcurrentMode) !== NoMode) || + current.memoizedState !== null) + ? HooksDispatcherOnUpdate + : HooksDispatcherOnMount; } let children = Component(props, secondArg); @@ -478,11 +521,25 @@ export function renderWithHooks( ReactCurrentDispatcher.current = ContextOnlyDispatcher; if (__DEV__) { + if ( + current !== null && + (current.mode & ConcurrentMode || current.memoizedState !== null) && + hookTypesDev != null && + hookTypesDev.length > 0 && + hookTypesUpdateIndexDev === -1 + ) { + // If we get here and have hookTypesDev, but the update index hasn't incremented, + // then none of the previously mounted hooks have been called, and we need to warn. + warnOnHookMismatchInDev(undefined, hookTypesUpdateIndexDev, hookTypesDev); + } + workInProgress._debugHookTypes = hookTypesDev; } // This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. + // This doesn't catch going from some hooks to no hooks, but that's not worth + // the additional check right now. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index bacf9da7029a7..8dc50b57ef53c 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -3847,6 +3847,43 @@ describe('ReactHooksWithNoopRenderer', () => { // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); }); + it('mount first state', () => { + function App(props) { + let A; + if (props.loadA) { + useState(0); + } else { + A = '[not loaded]'; + } + + return ; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A: [not loaded]']); + expect(ReactNoop.getChildren()).toEqual([span('A: [not loaded]')]); + + ReactNoop.render(); + expect(() => { + if (gate(flags => flags.enableThrowOnMountForHookMismatch)) { + expect(Scheduler).toFlushAndThrow( + 'Rendered more hooks than during the previous render.', + ); + } else { + expect(Scheduler).toFlushAndYield(['A: undefined']); + } + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. undefined useState\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + }); + it('unmount state', () => { let updateA; let updateB; @@ -3887,6 +3924,132 @@ describe('ReactHooksWithNoopRenderer', () => { ); }); + it('unmount last state', () => { + function App(props) { + let A; + if (props.loadA) { + useState(0); + } else { + A = '[not loaded]'; + } + + return ; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A: undefined']); + expect(ReactNoop.getChildren()).toEqual([span('A: undefined')]); + ReactNoop.render(); + + expect(() => { + // We don't throw because it would be noisy and require an additional prod check. + expect(Scheduler).not.toFlushAndThrow( + 'Rendered fewer hooks than expected. This may be caused by an ' + + 'accidental early return statement.', + ); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useState undefined\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + }); + + it('mount effect', () => { + function App(props) { + if (props.showMore) { + useEffect(() => { + Scheduler.unstable_yieldValue('Mount A'); + return () => { + Scheduler.unstable_yieldValue('Unmount A'); + }; + }, []); + } + + return null; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([]); + + act(() => { + ReactNoop.render(); + + if (gate(flags => flags.enableThrowOnMountForHookMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndThrow( + 'Rendered more hooks than during the previous render.', + ); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. undefined useEffect\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + } else { + expect(() => { + expect(Scheduler).toFlushAndYield(['Mount A']); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. undefined useEffect\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + + 'Warning: Internal React error: Expected static flag was missing. Please notify the React team.\n' + + ' in App (at **)', + ]); + } + }); + }); + + it('unmount effect', () => { + function App(props) { + if (props.showMore) { + useEffect(() => { + Scheduler.unstable_yieldValue('Mount A'); + return () => { + Scheduler.unstable_yieldValue('Unmount A'); + }; + }, []); + } + + return null; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Mount A']); + + ReactNoop.render(); + expect(() => { + // We don't throw because it would be noisy and require an additional prod check. + expect(Scheduler).not.toFlushAndThrow( + 'Rendered fewer hooks than expected. This may be caused by an ' + + 'accidental early return statement.', + ); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useEffect undefined\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + }); + it('unmount effects', () => { function App(props) { useEffect(() => { @@ -3920,9 +4083,9 @@ describe('ReactHooksWithNoopRenderer', () => { act(() => { ReactNoop.render(); expect(() => { - expect(() => { - expect(Scheduler).toFlushAndYield([]); - }).toThrow('Rendered more hooks than during the previous render'); + expect(Scheduler).toFlushAndThrow( + 'Rendered more hooks than during the previous render', + ); }).toErrorDev([ 'Warning: React has detected a change in the order of Hooks called by App. ' + 'This will lead to bugs and errors if not fixed. For more information, ' + diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 33d233f92e4ef..ff2655a9e12e2 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -103,6 +103,10 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = __EXPERIMENTAL__; +// When going from 0 to n hooks, throw "Rendered more/fewer" hooks than expected. +// This may be an invasive change so we're rolling it out slower. +export const enableThrowOnMountForHookMismatch = false; + // When a node is unmounted, recurse into the Fiber subtree and clean out // references. Each level cleans up more fiber fields than the previous level. // As far as we know, React itself doesn't leak, but because the Fiber contains diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0e07c9f67d994..4f12ed9b4c4ed 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -50,6 +50,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 933d914ce5a62..6ce3a57c03ddc 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index b218c53470bda..d2c8f39614689 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index b76a1b8506d13..fc4e201977d17 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableStrictEffects = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 63ba329f01a90..19af8c7b1c3ef 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = true; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 9e5a9107ab2b1..c46e8eb209751 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index a4c3e1ba32678..359ae1bda1cc8 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -40,6 +40,7 @@ export const warnAboutSpreadingKeyToJSX = false; export const enableSuspenseAvoidThisFallback = true; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; +export const enableThrowOnMountForHookMismatch = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 7a89e41ad54f2..2fdb1152e19fa 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -29,6 +29,7 @@ export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __ export const enableClientRenderFallbackOnTextMismatch = __VARIANT__; export const enableTransitionTracing = __VARIANT__; export const enableSymbolFallbackForWWW = __VARIANT__; +export const enableThrowOnMountForHookMismatch = __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 938d7589c4e21..204b7fb8603d2 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -33,6 +33,7 @@ export const { enableSyncDefaultUpdates, enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, enableClientRenderFallbackOnTextMismatch, + enableThrowOnMountForHookMismatch, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.