@samitouri / QOS-React-2 / commits / 5450dd4098

Strict Mode: Reuse memoized result from first pass (#25583)

In Strict Mode, during development, user functions are double invoked to help detect side effects. Currently, the way we implement this is to completely discard the first pass and start over. Theoretically this should be fine because components are idempotent. However, it's a bit tricky to get right because our implementation (i.e. `renderWithHooks`) is not completely idempotent with respect to internal data structures, like the work-in-progress fiber. In the past we've had to be really careful to avoid subtle bugs — for example, during the initial mount, `setState` functions are bound to the particular hook instances that were created during that render. If we compute new hook instances, we must also compute new children, and they must correspond to each other. This commit addresses a similar issue that came up related to `use`: when something suspends, `use` reuses the promise that was passed during the first attempt. This is itself a form of memoization. We need to be able to memoize the reactive inputs to the `use` call using a hook (i.e. `useMemo`), which means, the reactive inputs to `use` must come from the same component invocation as the output. The solution I've chosen is, rather than double invoke the entire `renderWithHook` function, we should double invoke each individual user function. It's a bit confusing but here's how it works: We will invoke the entire component function twice. However, during the second invocation of the component, the hook state from the first invocation will be reused. That means things like `useMemo` functions won't run again, because the deps will match and the memoized result will be reused. We want memoized functions to run twice, too, so account for this, user functions are double invoked during the *first* invocation of the component function, and are *not* double invoked during the second incovation: - First execution of component function: user functions are double invoked - Second execution of component function (in Strict Mode, during development): user functions are not double invoked. It's hard to explain verbally but much clearer when you run the test cases I've added.

Andrew Clark committed Oct 28, 2022 at 14:46 UTC 5450dd409863b31fa7ef4dfcf8aeb06ac16c4c10
5 files changed +455 -198
packages/react-reconciler/src/ReactFiberBeginWork.new.js
-58
@@ -418,25 +418,6 @@ function updateForwardRef(
418 renderLanes,
419 );
420 hasId = checkDidRenderIdHook();
421 - if (
422 - debugRenderPhaseSideEffectsForStrictMode &&
423 - workInProgress.mode & StrictLegacyMode
424 - ) {
425 - setIsStrictModeForDevtools(true);
426 - try {
427 - nextChildren = renderWithHooks(
428 - current,
429 - workInProgress,
430 - render,
431 - nextProps,
432 - ref,
433 - renderLanes,
434 - );
435 - hasId = checkDidRenderIdHook();
436 - } finally {
437 - setIsStrictModeForDevtools(false);
438 - }
439 - }
421 setIsRendering(false);
422 } else {
423 nextChildren = renderWithHooks(
@@ -1125,25 +1106,6 @@ function updateFunctionComponent(
1106 renderLanes,
1107 );
1108 hasId = checkDidRenderIdHook();
1128 - if (
1129 - debugRenderPhaseSideEffectsForStrictMode &&
1130 - workInProgress.mode & StrictLegacyMode
1131 - ) {
1132 - setIsStrictModeForDevtools(true);
1133 - try {
1134 - nextChildren = renderWithHooks(
1135 - current,
1136 - workInProgress,
1137 - Component,
1138 - nextProps,
1139 - context,
1140 - renderLanes,
1141 - );
1142 - hasId = checkDidRenderIdHook();
1143 - } finally {
1144 - setIsStrictModeForDevtools(false);
1145 - }
1146 - }
1109 setIsRendering(false);
1110 } else {
1111 nextChildren = renderWithHooks(
@@ -1969,26 +1931,6 @@ function mountIndeterminateComponent(
1931 getComponentNameFromType(Component) || 'Unknown',
1932 );
1933 }
1972 -
1973 - if (
1974 - debugRenderPhaseSideEffectsForStrictMode &&
1975 - workInProgress.mode & StrictLegacyMode
1976 - ) {
1977 - setIsStrictModeForDevtools(true);
1978 - try {
1979 - value = renderWithHooks(
1980 - null,
1981 - workInProgress,
1982 - Component,
1983 - props,
1984 - context,
1985 - renderLanes,
1986 - );
1987 - hasId = checkDidRenderIdHook();
1988 - } finally {
1989 - setIsStrictModeForDevtools(false);
1990 - }
1991 - }
1934 }
1935
1936 if (getIsHydrating() && hasId) {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
-58
@@ -418,25 +418,6 @@ function updateForwardRef(
418 renderLanes,
419 );
420 hasId = checkDidRenderIdHook();
421 - if (
422 - debugRenderPhaseSideEffectsForStrictMode &&
423 - workInProgress.mode & StrictLegacyMode
424 - ) {
425 - setIsStrictModeForDevtools(true);
426 - try {
427 - nextChildren = renderWithHooks(
428 - current,
429 - workInProgress,
430 - render,
431 - nextProps,
432 - ref,
433 - renderLanes,
434 - );
435 - hasId = checkDidRenderIdHook();
436 - } finally {
437 - setIsStrictModeForDevtools(false);
438 - }
439 - }
421 setIsRendering(false);
422 } else {
423 nextChildren = renderWithHooks(
@@ -1125,25 +1106,6 @@ function updateFunctionComponent(
1106 renderLanes,
1107 );
1108 hasId = checkDidRenderIdHook();
1128 - if (
1129 - debugRenderPhaseSideEffectsForStrictMode &&
1130 - workInProgress.mode & StrictLegacyMode
1131 - ) {
1132 - setIsStrictModeForDevtools(true);
1133 - try {
1134 - nextChildren = renderWithHooks(
1135 - current,
1136 - workInProgress,
1137 - Component,
1138 - nextProps,
1139 - context,
1140 - renderLanes,
1141 - );
1142 - hasId = checkDidRenderIdHook();
1143 - } finally {
1144 - setIsStrictModeForDevtools(false);
1145 - }
1146 - }
1109 setIsRendering(false);
1110 } else {
1111 nextChildren = renderWithHooks(
@@ -1969,26 +1931,6 @@ function mountIndeterminateComponent(
1931 getComponentNameFromType(Component) || 'Unknown',
1932 );
1933 }
1972 -
1973 - if (
1974 - debugRenderPhaseSideEffectsForStrictMode &&
1975 - workInProgress.mode & StrictLegacyMode
1976 - ) {
1977 - setIsStrictModeForDevtools(true);
1978 - try {
1979 - value = renderWithHooks(
1980 - null,
1981 - workInProgress,
1982 - Component,
1983 - props,
1984 - context,
1985 - renderLanes,
1986 - );
1987 - hasId = checkDidRenderIdHook();
1988 - } finally {
1989 - setIsStrictModeForDevtools(false);
1990 - }
1991 - }
1934 }
1935
1936 if (getIsHydrating() && hasId) {
packages/react-reconciler/src/ReactFiberHooks.new.js
+135 -41
@@ -41,6 +41,7 @@ import {
41 enableUseMemoCacheHook,
42 enableUseEventHook,
43 enableLegacyCache,
44 + debugRenderPhaseSideEffectsForStrictMode,
45 } from 'shared/ReactFeatureFlags';
46 import {
47 REACT_CONTEXT_TYPE,
@@ -53,6 +54,7 @@ import {
54 ConcurrentMode,
55 DebugTracingMode,
56 StrictEffectsMode,
57 + StrictLegacyMode,
58 } from './ReactTypeOfMode';
59 import {
60 NoLane,
@@ -121,7 +123,10 @@ import {
123 warnAboutMultipleRenderersDEV,
124 } from './ReactMutableSource.new';
125 import {logStateUpdateScheduled} from './DebugTracing';
124 -import {markStateUpdateScheduled} from './ReactFiberDevToolsHook.new';
126 +import {
127 + markStateUpdateScheduled,
128 + setIsStrictModeForDevtools,
129 +} from './ReactFiberDevToolsHook.new';
130 import {createCache} from './ReactFiberCacheComponent.new';
131 import {
132 createUpdate as createLegacyQueueUpdate,
@@ -140,6 +145,7 @@ import {
145 trackUsedThenable,
146 checkIfUseWrappedInTryCatch,
147 } from './ReactFiberThenable.new';
148 +import type {ThenableState} from './ReactFiberThenable.new';
149
150 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
151
@@ -236,6 +242,7 @@ let didScheduleRenderPhaseUpdate: boolean = false;
242 // TODO: Maybe there's some way to consolidate this with
243 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
244 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
245 +let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
246 // Counts the number of useId hooks in this component.
247 let localIdCounter: number = 0;
248 // Counts number of `use`-d thenables
@@ -473,50 +480,69 @@ export function renderWithHooks<Props, SecondArg>(
480 // If this is a replay, restore the thenable state from the previous attempt.
481 const prevThenableState = getSuspendedThenableState();
482 prepareThenableState(prevThenableState);
483 +
484 + // In Strict Mode, during development, user functions are double invoked to
485 + // help detect side effects. The logic for how this is implemented for in
486 + // hook components is a bit complex so let's break it down.
487 + //
488 + // We will invoke the entire component function twice. However, during the
489 + // second invocation of the component, the hook state from the first
490 + // invocation will be reused. That means things like `useMemo` functions won't
491 + // run again, because the deps will match and the memoized result will
492 + // be reused.
493 + //
494 + // We want memoized functions to run twice, too, so account for this, user
495 + // functions are double invoked during the *first* invocation of the component
496 + // function, and are *not* double invoked during the second incovation:
497 + //
498 + // - First execution of component function: user functions are double invoked
499 + // - Second execution of component function (in Strict Mode, during
500 + // development): user functions are not double invoked.
501 + //
502 + // This is intentional for a few reasons; most importantly, it's because of
503 + // how `use` works when something suspends: it reuses the promise that was
504 + // passed during the first attempt. This is itself a form of memoization.
505 + // We need to be able to memoize the reactive inputs to the `use` call using
506 + // a hook (i.e. `useMemo`), which means, the reactive inputs to `use` must
507 + // come from the same component invocation as the output.
508 + //
509 + // There are plenty of tests to ensure this behavior is correct.
510 + const shouldDoubleRenderDEV =
511 + __DEV__ &&
512 + debugRenderPhaseSideEffectsForStrictMode &&
513 + (workInProgress.mode & StrictLegacyMode) !== NoMode;
514 +
515 + shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
516 let children = Component(props, secondArg);
517 + shouldDoubleInvokeUserFnsInHooksDEV = false;
518
519 // Check if there was a render phase update
520 if (didScheduleRenderPhaseUpdateDuringThisPass) {
480 - // Keep rendering in a loop for as long as render phase updates continue to
481 - // be scheduled. Use a counter to prevent infinite loops.
482 - let numberOfReRenders: number = 0;
483 - do {
484 - didScheduleRenderPhaseUpdateDuringThisPass = false;
485 - localIdCounter = 0;
486 - thenableIndexCounter = 0;
487 -
488 - if (numberOfReRenders >= RE_RENDER_LIMIT) {
489 - throw new Error(
490 - 'Too many re-renders. React limits the number of renders to prevent ' +
491 - 'an infinite loop.',
492 - );
493 - }
494 -
495 - numberOfReRenders += 1;
496 - if (__DEV__) {
497 - // Even when hot reloading, allow dependencies to stabilize
498 - // after first render to prevent infinite render phase updates.
499 - ignorePreviousDependencies = false;
500 - }
501 -
502 - // Start over from the beginning of the list
503 - currentHook = null;
504 - workInProgressHook = null;
505 -
506 - workInProgress.updateQueue = null;
507 -
508 - if (__DEV__) {
509 - // Also validate hook order for cascading updates.
510 - hookTypesUpdateIndexDev = -1;
511 - }
512 -
513 - ReactCurrentDispatcher.current = __DEV__
514 - ? HooksDispatcherOnRerenderInDEV
515 - : HooksDispatcherOnRerender;
521 + // Keep rendering until the component stabilizes (there are no more render
522 + // phase updates).
523 + children = renderWithHooksAgain(
524 + workInProgress,
525 + Component,
526 + props,
527 + secondArg,
528 + prevThenableState,
529 + );
530 + }
531
517 - prepareThenableState(prevThenableState);
518 - children = Component(props, secondArg);
519 - } while (didScheduleRenderPhaseUpdateDuringThisPass);
532 + if (shouldDoubleRenderDEV) {
533 + // In development, components are invoked twice to help detect side effects.
534 + setIsStrictModeForDevtools(true);
535 + try {
536 + children = renderWithHooksAgain(
537 + workInProgress,
538 + Component,
539 + props,
540 + secondArg,
541 + prevThenableState,
542 + );
543 + } finally {
544 + setIsStrictModeForDevtools(false);
545 + }
546 }
547
548 // We can assume the previous dispatcher is always this one, since we set it
@@ -616,6 +642,65 @@ export function renderWithHooks<Props, SecondArg>(
642 return children;
643 }
644
645 +function renderWithHooksAgain<Props, SecondArg>(
646 + workInProgress: Fiber,
647 + Component: (p: Props, arg: SecondArg) => any,
648 + props: Props,
649 + secondArg: SecondArg,
650 + prevThenableState: ThenableState | null,
651 +) {
652 + // This is used to perform another render pass. It's used when setState is
653 + // called during render, and for double invoking components in Strict Mode
654 + // during development.
655 + //
656 + // The state from the previous pass is reused whenever possible. So, state
657 + // updates that were already processed are not processed again, and memoized
658 + // functions (`useMemo`) are not invoked again.
659 + //
660 + // Keep rendering in a loop for as long as render phase updates continue to
661 + // be scheduled. Use a counter to prevent infinite loops.
662 + let numberOfReRenders: number = 0;
663 + let children;
664 + do {
665 + didScheduleRenderPhaseUpdateDuringThisPass = false;
666 + localIdCounter = 0;
667 + thenableIndexCounter = 0;
668 +
669 + if (numberOfReRenders >= RE_RENDER_LIMIT) {
670 + throw new Error(
671 + 'Too many re-renders. React limits the number of renders to prevent ' +
672 + 'an infinite loop.',
673 + );
674 + }
675 +
676 + numberOfReRenders += 1;
677 + if (__DEV__) {
678 + // Even when hot reloading, allow dependencies to stabilize
679 + // after first render to prevent infinite render phase updates.
680 + ignorePreviousDependencies = false;
681 + }
682 +
683 + // Start over from the beginning of the list
684 + currentHook = null;
685 + workInProgressHook = null;
686 +
687 + workInProgress.updateQueue = null;
688 +
689 + if (__DEV__) {
690 + // Also validate hook order for cascading updates.
691 + hookTypesUpdateIndexDev = -1;
692 + }
693 +
694 + ReactCurrentDispatcher.current = __DEV__
695 + ? HooksDispatcherOnRerenderInDEV
696 + : HooksDispatcherOnRerender;
697 +
698 + prepareThenableState(prevThenableState);
699 + children = Component(props, secondArg);
700 + } while (didScheduleRenderPhaseUpdateDuringThisPass);
701 + return children;
702 +}
703 +
704 export function checkDidRenderIdHook(): boolean {
705 // This should be called immediately after every renderWithHooks call.
706 // Conceptually, it's part of the return value of renderWithHooks; it's only a
@@ -1023,12 +1108,15 @@ function updateReducer<S, I, A>(
1108 }
1109
1110 // Process this update.
1111 + const action = update.action;
1112 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
1113 + reducer(newState, action);
1114 + }
1115 if (update.hasEagerState) {
1116 // If this update is a state update (not a reducer) and was processed eagerly,
1117 // we can use the eagerly computed state
1118 newState = ((update.eagerState: any): S);
1119 } else {
1031 - const action = update.action;
1120 newState = reducer(newState, action);
1121 }
1122 }
@@ -2110,6 +2198,9 @@ function mountMemo<T>(
2198 ): T {
2199 const hook = mountWorkInProgressHook();
2200 const nextDeps = deps === undefined ? null : deps;
2201 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
2202 + nextCreate();
2203 + }
2204 const nextValue = nextCreate();
2205 hook.memoizedState = [nextValue, nextDeps];
2206 return nextValue;
@@ -2131,6 +2222,9 @@ function updateMemo<T>(
2222 }
2223 }
2224 }
2225 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
2226 + nextCreate();
2227 + }
2228 const nextValue = nextCreate();
2229 hook.memoizedState = [nextValue, nextDeps];
2230 return nextValue;
packages/react-reconciler/src/ReactFiberHooks.old.js
+135 -41
@@ -41,6 +41,7 @@ import {
41 enableUseMemoCacheHook,
42 enableUseEventHook,
43 enableLegacyCache,
44 + debugRenderPhaseSideEffectsForStrictMode,
45 } from 'shared/ReactFeatureFlags';
46 import {
47 REACT_CONTEXT_TYPE,
@@ -53,6 +54,7 @@ import {
54 ConcurrentMode,
55 DebugTracingMode,
56 StrictEffectsMode,
57 + StrictLegacyMode,
58 } from './ReactTypeOfMode';
59 import {
60 NoLane,
@@ -121,7 +123,10 @@ import {
123 warnAboutMultipleRenderersDEV,
124 } from './ReactMutableSource.old';
125 import {logStateUpdateScheduled} from './DebugTracing';
124 -import {markStateUpdateScheduled} from './ReactFiberDevToolsHook.old';
126 +import {
127 + markStateUpdateScheduled,
128 + setIsStrictModeForDevtools,
129 +} from './ReactFiberDevToolsHook.old';
130 import {createCache} from './ReactFiberCacheComponent.old';
131 import {
132 createUpdate as createLegacyQueueUpdate,
@@ -140,6 +145,7 @@ import {
145 trackUsedThenable,
146 checkIfUseWrappedInTryCatch,
147 } from './ReactFiberThenable.old';
148 +import type {ThenableState} from './ReactFiberThenable.old';
149
150 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
151
@@ -236,6 +242,7 @@ let didScheduleRenderPhaseUpdate: boolean = false;
242 // TODO: Maybe there's some way to consolidate this with
243 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
244 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
245 +let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
246 // Counts the number of useId hooks in this component.
247 let localIdCounter: number = 0;
248 // Counts number of `use`-d thenables
@@ -473,50 +480,69 @@ export function renderWithHooks<Props, SecondArg>(
480 // If this is a replay, restore the thenable state from the previous attempt.
481 const prevThenableState = getSuspendedThenableState();
482 prepareThenableState(prevThenableState);
483 +
484 + // In Strict Mode, during development, user functions are double invoked to
485 + // help detect side effects. The logic for how this is implemented for in
486 + // hook components is a bit complex so let's break it down.
487 + //
488 + // We will invoke the entire component function twice. However, during the
489 + // second invocation of the component, the hook state from the first
490 + // invocation will be reused. That means things like `useMemo` functions won't
491 + // run again, because the deps will match and the memoized result will
492 + // be reused.
493 + //
494 + // We want memoized functions to run twice, too, so account for this, user
495 + // functions are double invoked during the *first* invocation of the component
496 + // function, and are *not* double invoked during the second incovation:
497 + //
498 + // - First execution of component function: user functions are double invoked
499 + // - Second execution of component function (in Strict Mode, during
500 + // development): user functions are not double invoked.
501 + //
502 + // This is intentional for a few reasons; most importantly, it's because of
503 + // how `use` works when something suspends: it reuses the promise that was
504 + // passed during the first attempt. This is itself a form of memoization.
505 + // We need to be able to memoize the reactive inputs to the `use` call using
506 + // a hook (i.e. `useMemo`), which means, the reactive inputs to `use` must
507 + // come from the same component invocation as the output.
508 + //
509 + // There are plenty of tests to ensure this behavior is correct.
510 + const shouldDoubleRenderDEV =
511 + __DEV__ &&
512 + debugRenderPhaseSideEffectsForStrictMode &&
513 + (workInProgress.mode & StrictLegacyMode) !== NoMode;
514 +
515 + shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
516 let children = Component(props, secondArg);
517 + shouldDoubleInvokeUserFnsInHooksDEV = false;
518
519 // Check if there was a render phase update
520 if (didScheduleRenderPhaseUpdateDuringThisPass) {
480 - // Keep rendering in a loop for as long as render phase updates continue to
481 - // be scheduled. Use a counter to prevent infinite loops.
482 - let numberOfReRenders: number = 0;
483 - do {
484 - didScheduleRenderPhaseUpdateDuringThisPass = false;
485 - localIdCounter = 0;
486 - thenableIndexCounter = 0;
487 -
488 - if (numberOfReRenders >= RE_RENDER_LIMIT) {
489 - throw new Error(
490 - 'Too many re-renders. React limits the number of renders to prevent ' +
491 - 'an infinite loop.',
492 - );
493 - }
494 -
495 - numberOfReRenders += 1;
496 - if (__DEV__) {
497 - // Even when hot reloading, allow dependencies to stabilize
498 - // after first render to prevent infinite render phase updates.
499 - ignorePreviousDependencies = false;
500 - }
501 -
502 - // Start over from the beginning of the list
503 - currentHook = null;
504 - workInProgressHook = null;
505 -
506 - workInProgress.updateQueue = null;
507 -
508 - if (__DEV__) {
509 - // Also validate hook order for cascading updates.
510 - hookTypesUpdateIndexDev = -1;
511 - }
512 -
513 - ReactCurrentDispatcher.current = __DEV__
514 - ? HooksDispatcherOnRerenderInDEV
515 - : HooksDispatcherOnRerender;
521 + // Keep rendering until the component stabilizes (there are no more render
522 + // phase updates).
523 + children = renderWithHooksAgain(
524 + workInProgress,
525 + Component,
526 + props,
527 + secondArg,
528 + prevThenableState,
529 + );
530 + }
531
517 - prepareThenableState(prevThenableState);
518 - children = Component(props, secondArg);
519 - } while (didScheduleRenderPhaseUpdateDuringThisPass);
532 + if (shouldDoubleRenderDEV) {
533 + // In development, components are invoked twice to help detect side effects.
534 + setIsStrictModeForDevtools(true);
535 + try {
536 + children = renderWithHooksAgain(
537 + workInProgress,
538 + Component,
539 + props,
540 + secondArg,
541 + prevThenableState,
542 + );
543 + } finally {
544 + setIsStrictModeForDevtools(false);
545 + }
546 }
547
548 // We can assume the previous dispatcher is always this one, since we set it
@@ -616,6 +642,65 @@ export function renderWithHooks<Props, SecondArg>(
642 return children;
643 }
644
645 +function renderWithHooksAgain<Props, SecondArg>(
646 + workInProgress: Fiber,
647 + Component: (p: Props, arg: SecondArg) => any,
648 + props: Props,
649 + secondArg: SecondArg,
650 + prevThenableState: ThenableState | null,
651 +) {
652 + // This is used to perform another render pass. It's used when setState is
653 + // called during render, and for double invoking components in Strict Mode
654 + // during development.
655 + //
656 + // The state from the previous pass is reused whenever possible. So, state
657 + // updates that were already processed are not processed again, and memoized
658 + // functions (`useMemo`) are not invoked again.
659 + //
660 + // Keep rendering in a loop for as long as render phase updates continue to
661 + // be scheduled. Use a counter to prevent infinite loops.
662 + let numberOfReRenders: number = 0;
663 + let children;
664 + do {
665 + didScheduleRenderPhaseUpdateDuringThisPass = false;
666 + localIdCounter = 0;
667 + thenableIndexCounter = 0;
668 +
669 + if (numberOfReRenders >= RE_RENDER_LIMIT) {
670 + throw new Error(
671 + 'Too many re-renders. React limits the number of renders to prevent ' +
672 + 'an infinite loop.',
673 + );
674 + }
675 +
676 + numberOfReRenders += 1;
677 + if (__DEV__) {
678 + // Even when hot reloading, allow dependencies to stabilize
679 + // after first render to prevent infinite render phase updates.
680 + ignorePreviousDependencies = false;
681 + }
682 +
683 + // Start over from the beginning of the list
684 + currentHook = null;
685 + workInProgressHook = null;
686 +
687 + workInProgress.updateQueue = null;
688 +
689 + if (__DEV__) {
690 + // Also validate hook order for cascading updates.
691 + hookTypesUpdateIndexDev = -1;
692 + }
693 +
694 + ReactCurrentDispatcher.current = __DEV__
695 + ? HooksDispatcherOnRerenderInDEV
696 + : HooksDispatcherOnRerender;
697 +
698 + prepareThenableState(prevThenableState);
699 + children = Component(props, secondArg);
700 + } while (didScheduleRenderPhaseUpdateDuringThisPass);
701 + return children;
702 +}
703 +
704 export function checkDidRenderIdHook(): boolean {
705 // This should be called immediately after every renderWithHooks call.
706 // Conceptually, it's part of the return value of renderWithHooks; it's only a
@@ -1023,12 +1108,15 @@ function updateReducer<S, I, A>(
1108 }
1109
1110 // Process this update.
1111 + const action = update.action;
1112 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
1113 + reducer(newState, action);
1114 + }
1115 if (update.hasEagerState) {
1116 // If this update is a state update (not a reducer) and was processed eagerly,
1117 // we can use the eagerly computed state
1118 newState = ((update.eagerState: any): S);
1119 } else {
1031 - const action = update.action;
1120 newState = reducer(newState, action);
1121 }
1122 }
@@ -2110,6 +2198,9 @@ function mountMemo<T>(
2198 ): T {
2199 const hook = mountWorkInProgressHook();
2200 const nextDeps = deps === undefined ? null : deps;
2201 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
2202 + nextCreate();
2203 + }
2204 const nextValue = nextCreate();
2205 hook.memoizedState = [nextValue, nextDeps];
2206 return nextValue;
@@ -2131,6 +2222,9 @@ function updateMemo<T>(
2222 }
2223 }
2224 }
2225 + if (shouldDoubleInvokeUserFnsInHooksDEV) {
2226 + nextCreate();
2227 + }
2228 const nextValue = nextCreate();
2229 hook.memoizedState = [nextValue, nextDeps];
2230 return nextValue;
packages/react/src/__tests__/ReactStrictMode-test.js
+185
@@ -15,6 +15,10 @@ let ReactDOMClient;
15 let ReactDOMServer;
16 let Scheduler;
17 let PropTypes;
18 +let act;
19 +let useMemo;
20 +let useState;
21 +let useReducer;
22
23 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
24
@@ -25,6 +29,10 @@ describe('ReactStrictMode', () => {
29 ReactDOM = require('react-dom');
30 ReactDOMClient = require('react-dom/client');
31 ReactDOMServer = require('react-dom/server');
32 + act = require('jest-react').act;
33 + useMemo = React.useMemo;
34 + useState = React.useState;
35 + useReducer = React.useReducer;
36 });
37
38 it('should appear in the client component stack', () => {
@@ -331,6 +339,183 @@ describe('ReactStrictMode', () => {
339 // But each time `state` should be the previous value
340 expect(instance.state.count).toBe(2);
341 });
342 +
343 + // @gate debugRenderPhaseSideEffectsForStrictMode
344 + it('double invokes useMemo functions', async () => {
345 + let log = [];
346 +
347 + function Uppercased({text}) {
348 + return useMemo(() => {
349 + const uppercased = text.toUpperCase();
350 + log.push('Compute toUpperCase: ' + uppercased);
351 + return uppercased;
352 + }, [text]);
353 + }
354 +
355 + const container = document.createElement('div');
356 + const root = ReactDOMClient.createRoot(container);
357 +
358 + // Mount
359 + await act(() => {
360 + root.render(
361 + <React.StrictMode>
362 + <Uppercased text="hello" />
363 + </React.StrictMode>,
364 + );
365 + });
366 + expect(container.textContent).toBe('HELLO');
367 + expect(log).toEqual([
368 + 'Compute toUpperCase: HELLO',
369 + 'Compute toUpperCase: HELLO',
370 + ]);
371 +
372 + log = [];
373 +
374 + // Update
375 + await act(() => {
376 + root.render(
377 + <React.StrictMode>
378 + <Uppercased text="goodbye" />
379 + </React.StrictMode>,
380 + );
381 + });
382 + expect(container.textContent).toBe('GOODBYE');
383 + expect(log).toEqual([
384 + 'Compute toUpperCase: GOODBYE',
385 + 'Compute toUpperCase: GOODBYE',
386 + ]);
387 + });
388 +
389 + // @gate debugRenderPhaseSideEffectsForStrictMode
390 + it('double invokes useMemo functions', async () => {
391 + let log = [];
392 + function Uppercased({text}) {
393 + const memoizedResult = useMemo(() => {
394 + const uppercased = text.toUpperCase();
395 + log.push('Compute toUpperCase: ' + uppercased);
396 + return {uppercased};
397 + }, [text]);
398 +
399 + // Push this to the log so we can check whether the same memoized result
400 + // it returned during both invocations.
401 + log.push(memoizedResult);
402 +
403 + return memoizedResult.uppercased;
404 + }
405 +
406 + const container = document.createElement('div');
407 + const root = ReactDOMClient.createRoot(container);
408 +
409 + // Mount
410 + await act(() => {
411 + root.render(
412 + <React.StrictMode>
413 + <Uppercased text="hello" />
414 + </React.StrictMode>,
415 + );
416 + });
417 + expect(container.textContent).toBe('HELLO');
418 + expect(log).toEqual([
419 + 'Compute toUpperCase: HELLO',
420 + 'Compute toUpperCase: HELLO',
421 + {uppercased: 'HELLO'},
422 + {uppercased: 'HELLO'},
423 + ]);
424 +
425 + // Even though the memoized function is invoked twice, the same object
426 + // is returned both times.
427 + expect(log[2]).toBe(log[3]);
428 +
429 + log = [];
430 +
431 + // Update
432 + await act(() => {
433 + root.render(
434 + <React.StrictMode>
435 + <Uppercased text="goodbye" />
436 + </React.StrictMode>,
437 + );
438 + });
439 + expect(container.textContent).toBe('GOODBYE');
440 + expect(log).toEqual([
441 + 'Compute toUpperCase: GOODBYE',
442 + 'Compute toUpperCase: GOODBYE',
443 + {uppercased: 'GOODBYE'},
444 + {uppercased: 'GOODBYE'},
445 + ]);
446 +
447 + // Even though the memoized function is invoked twice, the same object
448 + // is returned both times.
449 + expect(log[2]).toBe(log[3]);
450 + });
451 +
452 + // @gate debugRenderPhaseSideEffectsForStrictMode
453 + it('double invokes setState updater functions', async () => {
454 + const log = [];
455 +
456 + let setCount;
457 + function App() {
458 + const [count, _setCount] = useState(0);
459 + setCount = _setCount;
460 + return count;
461 + }
462 +
463 + const container = document.createElement('div');
464 + const root = ReactDOMClient.createRoot(container);
465 +
466 + await act(() => {
467 + root.render(
468 + <React.StrictMode>
469 + <App />
470 + </React.StrictMode>,
471 + );
472 + });
473 + expect(container.textContent).toBe('0');
474 +
475 + await act(() => {
476 + setCount(() => {
477 + log.push('Compute count: 1');
478 + return 1;
479 + });
480 + });
481 + expect(container.textContent).toBe('1');
482 + expect(log).toEqual(['Compute count: 1', 'Compute count: 1']);
483 + });
484 +
485 + // @gate debugRenderPhaseSideEffectsForStrictMode
486 + it('double invokes reducer functions', async () => {
487 + const log = [];
488 +
489 + function reducer(prevState, action) {
490 + log.push('Compute new state: ' + action);
491 + return action;
492 + }
493 +
494 + let dispatch;
495 + function App() {
496 + const [count, _dispatch] = useReducer(reducer, 0);
497 + dispatch = _dispatch;
498 + return count;
499 + }
500 +
501 + const container = document.createElement('div');
502 + const root = ReactDOMClient.createRoot(container);
503 +
504 + await act(() => {
505 + root.render(
506 + <React.StrictMode>
507 + <App />
508 + </React.StrictMode>,
509 + );
510 + });
511 + expect(container.textContent).toBe('0');
512 +
513 + await act(() => {
514 + dispatch(1);
515 + });
516 + expect(container.textContent).toBe('1');
517 + expect(log).toEqual(['Compute new state: 1', 'Compute new state: 1']);
518 + });
519 });
520
521 describe('Concurrent Mode', () => {