Repository: facebook/react
Pull request: Strict Mode: Reuse memoized result from first pass

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.

Changed files:
filename: packages/react-reconciler/src/ReactFiberBeginWork.new.js
patch: @@ -418,25 +418,6 @@ function updateForwardRef(
       renderLanes,
     );
     hasId = checkDidRenderIdHook();
-    if (
-      debugRenderPhaseSideEffectsForStrictMode &&
-      workInProgress.mode & StrictLegacyMode
-    ) {
-      setIsStrictModeForDevtools(true);
-      try {
-        nextChildren = renderWithHooks(
-          current,
-          workInProgress,
-          render,
-          nextProps,
-          ref,
-          renderLanes,
-        );
-        hasId = checkDidRenderIdHook();
-      } finally {
-        setIsStrictModeForDevtools(false);
-      }
-    }
     setIsRendering(false);
   } else {
     nextChildren = renderWithHooks(
@@ -1125,25 +1106,6 @@ function updateFunctionComponent(
       renderLanes,
     );
     hasId = checkDidRenderIdHook();
-    if (
-      debugRenderPhaseSideEffectsForStrictMode &&
-      workInProgress.mode & StrictLegacyMode
-    ) {
-      setIsStrictModeForDevtools(true);
-      try {
-        nextChildren = renderWithHooks(
-          current,
-          workInProgress,
-          Component,
-          nextProps,
-          context,
-          renderLanes,
-        );
-        hasId = checkDidRenderIdHook();
-      } finally {
-        setIsStrictModeForDevtools(false);
-      }
-    }
     setIsRendering(false);
   } else {
     nextChildren = renderWithHooks(
@@ -1969,26 +1931,6 @@ function mountIndeterminateComponent(
           getComponentNameFromType(Component) || 'Unknown',
         );
       }
-
-      if (
-        debugRenderPhaseSideEffectsForStrictMode &&
-        workInProgress.mode & StrictLegacyMode
-      ) {
-        setIsStrictModeForDevtools(true);
-        try {
-          value = renderWithHooks(
-            null,
-            workInProgress,
-            Component,
-            props,
-            context,
-            renderLanes,
-          );
-          hasId = checkDidRenderIdHook();
-        } finally {
-          setIsStrictModeForDevtools(false);
-        }
-      }
     }
 
     if (getIsHydrating() && hasId) {

filename: packages/react-reconciler/src/ReactFiberBeginWork.old.js
patch: @@ -418,25 +418,6 @@ function updateForwardRef(
       renderLanes,
     );
     hasId = checkDidRenderIdHook();
-    if (
-      debugRenderPhaseSideEffectsForStrictMode &&
-      workInProgress.mode & StrictLegacyMode
-    ) {
-      setIsStrictModeForDevtools(true);
-      try {
-        nextChildren = renderWithHooks(
-          current,
-          workInProgress,
-          render,
-          nextProps,
-          ref,
-          renderLanes,
-        );
-        hasId = checkDidRenderIdHook();
-      } finally {
-        setIsStrictModeForDevtools(false);
-      }
-    }
     setIsRendering(false);
   } else {
     nextChildren = renderWithHooks(
@@ -1125,25 +1106,6 @@ function updateFunctionComponent(
       renderLanes,
     );
     hasId = checkDidRenderIdHook();
-    if (
-      debugRenderPhaseSideEffectsForStrictMode &&
-      workInProgress.mode & StrictLegacyMode
-    ) {
-      setIsStrictModeForDevtools(true);
-      try {
-        nextChildren = renderWithHooks(
-          current,
-          workInProgress,
-          Component,
-          nextProps,
-          context,
-          renderLanes,
-        );
-        hasId = checkDidRenderIdHook();
-      } finally {
-        setIsStrictModeForDevtools(false);
-      }
-    }
     setIsRendering(false);
   } else {
     nextChildren = renderWithHooks(
@@ -1969,26 +1931,6 @@ function mountIndeterminateComponent(
           getComponentNameFromType(Component) || 'Unknown',
         );
       }
-
-      if (
-        debugRenderPhaseSideEffectsForStrictMode &&
-        workInProgress.mode & StrictLegacyMode
-      ) {
-        setIsStrictModeForDevtools(true);
-        try {
-          value = renderWithHooks(
-            null,
-            workInProgress,
-            Component,
-            props,
-            context,
-            renderLanes,
-          );
-          hasId = checkDidRenderIdHook();
-        } finally {
-          setIsStrictModeForDevtools(false);
-        }
-      }
     }
 
     if (getIsHydrating() && hasId) {

filename: packages/react-reconciler/src/ReactFiberHooks.new.js
patch: @@ -41,6 +41,7 @@ import {
   enableUseMemoCacheHook,
   enableUseEventHook,
   enableLegacyCache,
+  debugRenderPhaseSideEffectsForStrictMode,
 } from 'shared/ReactFeatureFlags';
 import {
   REACT_CONTEXT_TYPE,
@@ -53,6 +54,7 @@ import {
   ConcurrentMode,
   DebugTracingMode,
   StrictEffectsMode,
+  StrictLegacyMode,
 } from './ReactTypeOfMode';
 import {
   NoLane,
@@ -121,7 +123,10 @@ import {
   warnAboutMultipleRenderersDEV,
 } from './ReactMutableSource.new';
 import {logStateUpdateScheduled} from './DebugTracing';
-import {markStateUpdateScheduled} from './ReactFiberDevToolsHook.new';
+import {
+  markStateUpdateScheduled,
+  setIsStrictModeForDevtools,
+} from './ReactFiberDevToolsHook.new';
 import {createCache} from './ReactFiberCacheComponent.new';
 import {
   createUpdate as createLegacyQueueUpdate,
@@ -140,6 +145,7 @@ import {
   trackUsedThenable,
   checkIfUseWrappedInTryCatch,
 } from './ReactFiberThenable.new';
+import type {ThenableState} from './ReactFiberThenable.new';
 
 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
 
@@ -236,6 +242,7 @@ let didScheduleRenderPhaseUpdate: boolean = false;
 // TODO: Maybe there's some way to consolidate this with
 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
+let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
 // Counts the number of useId hooks in this component.
 let localIdCounter: number = 0;
 // Counts number of `use`-d thenables
@@ -473,50 +480,69 @@ export function renderWithHooks<Props, SecondArg>(
   // If this is a replay, restore the thenable state from the previous attempt.
   const prevThenableState = getSuspendedThenableState();
   prepareThenableState(prevThenableState);
+
+  // In Strict Mode, during development, user functions are double invoked to
+  // help detect side effects. The logic for how this is implemented for in
+  // hook components is a bit complex so let's break it down.
+  //
+  // 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.
+  //
+  // This is intentional for a few reasons; most importantly, it's because of
+  // how `use` works when something suspends: it 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.
+  //
+  // There are plenty of tests to ensure this behavior is correct.
+  const shouldDoubleRenderDEV =
+    __DEV__ &&
+    debugRenderPhaseSideEffectsForStrictMode &&
+    (workInProgress.mode & StrictLegacyMode) !== NoMode;
+
+  shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
   let children = Component(props, secondArg);
+  shouldDoubleInvokeUserFnsInHooksDEV = false;
 
   // Check if there was a render phase update
   if (didScheduleRenderPhaseUpdateDuringThisPass) {
-    // Keep rendering in a loop for as long as render phase updates continue to
-    // be scheduled. Use a counter to prevent infinite loops.
-    let numberOfReRenders: number = 0;
-    do {
-      didScheduleRenderPhaseUpdateDuringThisPass = false;
-      localIdCounter = 0;
-      thenableIndexCounter = 0;
-
-      if (numberOfReRenders >= RE_RENDER_LIMIT) {
-        throw new Error(
-          'Too many re-renders. React limits the number of renders to prevent ' +
-            'an infinite loop.',
-        );
-      }
-
-      numberOfReRenders += 1;
-      if (__DEV__) {
-        // Even when hot reloading, allow dependencies to stabilize
-        // after first render to prevent infinite render phase updates.
-        ignorePreviousDependencies = false;
-      }
-
-      // Start over from the beginning of the list
-      currentHook = null;
-      workInProgressHook = null;
-
-      workInProgress.updateQueue = null;
-
-      if (__DEV__) {
-        // Also validate hook order for cascading updates.
-        hookTypesUpdateIndexDev = -1;
-      }
-
-      ReactCurrentDispatcher.current = __DEV__
-        ? HooksDispatcherOnRerenderInDEV
-        : HooksDispatcherOnRerender;
+    // Keep rendering until the component stabilizes (there are no more render
+    // phase updates).
+    children = renderWithHooksAgain(
+      workInProgress,
+      Component,
+      props,
+      secondArg,
+      prevThenableState,
+    );
+  }
 
-      prepareThenableState(prevThenableState);
-      children = Component(props, secondArg);
-    } while (didScheduleRenderPhaseUpdateDuringThisPass);
+  if (shouldDoubleRenderDEV) {
+    // In development, components are invoked twice to help detect side effects.
+    setIsStrictModeForDevtools(true);
+    try {
+      children = renderWithHooksAgain(
+        workInProgress,
+        Component,
+        props,
+        secondArg,
+        prevThenableState,
+      );
+    } finally {
+      setIsStrictModeForDevtools(false);
+    }
   }
 
   // We can assume the previous dispatcher is always this one, since we set it
@@ -616,6 +642,65 @@ export function renderWithHooks<Props, SecondArg>(
   return children;
 }
 
+function renderWithHooksAgain<Props, SecondArg>(
+  workInProgress: Fiber,
+  Component: (p: Props, arg: SecondArg) => any,
+  props: Props,
+  secondArg: SecondArg,
+  prevThenableState: ThenableState | null,
+) {
+  // This is used to perform another render pass. It's used when setState is
+  // called during render, and for double invoking components in Strict Mode
+  // during development.
+  //
+  // The state from the previous pass is reused whenever possible. So, state
+  // updates that were already processed are not processed again, and memoized
+  // functions (`useMemo`) are not invoked again.
+  //
+  // Keep rendering in a loop for as long as render phase updates continue to
+  // be scheduled. Use a counter to prevent infinite loops.
+  let numberOfReRenders: number = 0;
+  let children;
+  do {
+    didScheduleRenderPhaseUpdateDuringThisPass = false;
+    localIdCounter = 0;
+    thenableIndexCounter = 0;
+
+    if (numberOfReRenders >= RE_RENDER_LIMIT) {
+      throw new Error(
+        'Too many re-renders. React limits the number of renders to prevent ' +
+          'an infinite loop.',
+      );
+    }
+
+    numberOfReRenders += 1;
+    if (__DEV__) {
+      // Even when hot reloading, allow dependencies to stabilize
+      // after first render to prevent infinite render phase updates.
+      ignorePreviousDependencies = false;
+    }
+
+    // Start over from the beginning of the list
+    currentHook = null;
+    workInProgressHook = null;
+
+    workInProgress.updateQueue = null;
+
+    if (__DEV__) {
+      // Also validate hook order for cascading updates.
+      hookTypesUpdateIndexDev = -1;
+    }
+
+    ReactCurrentDispatcher.current = __DEV__
+      ? HooksDispatcherOnRerenderInDEV
+      : HooksDispatcherOnRerender;
+
+    prepareThenableState(prevThenableState);
+    children = Component(props, secondArg);
+  } while (didScheduleRenderPhaseUpdateDuringThisPass);
+  return children;
+}
+
 export function checkDidRenderIdHook(): boolean {
   // This should be called immediately after every renderWithHooks call.
   // Conceptually, it's part of the return value of renderWithHooks; it's only a
@@ -1023,12 +1108,15 @@ function updateReducer<S, I, A>(
         }
 
         // Process this update.
+        const action = update.action;
+        if (shouldDoubleInvokeUserFnsInHooksDEV) {
+          reducer(newState, action);
+        }
         if (update.hasEagerState) {
           // If this update is a state update (not a reducer) and was processed eagerly,
           // we can use the eagerly computed state
           newState = ((update.eagerState: any): S);
         } else {
-          const action = update.action;
           newState = reducer(newState, action);
         }
       }
@@ -2110,6 +2198,9 @@ function mountMemo<T>(
 ): T {
   const hook = mountWorkInProgressHook();
   const nextDeps = deps === undefined ? null : deps;
+  if (shouldDoubleInvokeUserFnsInHooksDEV) {
+    nextCreate();
+  }
   const nextValue = nextCreate();
   hook.memoizedState = [nextValue, nextDeps];
   return nextValue;
@@ -2131,6 +2222,9 @@ function updateMemo<T>(
       }
     }
   }
+  if (shouldDoubleInvokeUserFnsInHooksDEV) {
+    nextCreate();
+  }
   const nextValue = nextCreate();
   hook.memoizedState = [nextValue, nextDeps];
   return nextValue;

filename: packages/react-reconciler/src/ReactFiberHooks.old.js
patch: @@ -41,6 +41,7 @@ import {
   enableUseMemoCacheHook,
   enableUseEventHook,
   enableLegacyCache,
+  debugRenderPhaseSideEffectsForStrictMode,
 } from 'shared/ReactFeatureFlags';
 import {
   REACT_CONTEXT_TYPE,
@@ -53,6 +54,7 @@ import {
   ConcurrentMode,
   DebugTracingMode,
   StrictEffectsMode,
+  StrictLegacyMode,
 } from './ReactTypeOfMode';
 import {
   NoLane,
@@ -121,7 +123,10 @@ import {
   warnAboutMultipleRenderersDEV,
 } from './ReactMutableSource.old';
 import {logStateUpdateScheduled} from './DebugTracing';
-import {markStateUpdateScheduled} from './ReactFiberDevToolsHook.old';
+import {
+  markStateUpdateScheduled,
+  setIsStrictModeForDevtools,
+} from './ReactFiberDevToolsHook.old';
 import {createCache} from './ReactFiberCacheComponent.old';
 import {
   createUpdate as createLegacyQueueUpdate,
@@ -140,6 +145,7 @@ import {
   trackUsedThenable,
   checkIfUseWrappedInTryCatch,
 } from './ReactFiberThenable.old';
+import type {ThenableState} from './ReactFiberThenable.old';
 
 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
 
@@ -236,6 +242,7 @@ let didScheduleRenderPhaseUpdate: boolean = false;
 // TODO: Maybe there's some way to consolidate this with
 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
+let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
 // Counts the number of useId hooks in this component.
 let localIdCounter: number = 0;
 // Counts number of `use`-d thenables
@@ -473,50 +480,69 @@ export function renderWithHooks<Props, SecondArg>(
   // If this is a replay, restore the thenable state from the previous attempt.
   const prevThenableState = getSuspendedThenableState();
   prepareThenableState(prevThenableState);
+
+  // In Strict Mode, during development, user functions are double invoked to
+  // help detect side effects. The logic for how this is implemented for in
+  // hook components is a bit complex so let's break it down.
+  //
+  // 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.
+  //
+  // This is intentional for a few reasons; most importantly, it's because of
+  // how `use` works when something suspends: it 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.
+  //
+  // There are plenty of tests to ensure this behavior is correct.
+  const shouldDoubleRenderDEV =
+    __DEV__ &&
+    debugRenderPhaseSideEffectsForStrictMode &&
+    (workInProgress.mode & StrictLegacyMode) !== NoMode;
+
+  shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
   let children = Component(props, secondArg);
+  shouldDoubleInvokeUserFnsInHooksDEV = false;
 
   // Check if there was a render phase update
   if (didScheduleRenderPhaseUpdateDuringThisPass) {
-    // Keep rendering in a loop for as long as render phase updates continue to
-    // be scheduled. Use a counter to prevent infinite loops.
-    let numberOfReRenders: number = 0;
-    do {
-      didScheduleRenderPhaseUpdateDuringThisPass = false;
-      localIdCounter = 0;
-      thenableIndexCounter = 0;
-
-      if (numberOfReRenders >= RE_RENDER_LIMIT) {
-        throw new Error(
-          'Too many re-renders. React limits the number of renders to prevent ' +
-            'an infinite loop.',
-        );
-      }
-
-      numberOfReRenders += 1;
-      if (__DEV__) {
-        // Even when hot reloading, allow dependencies to stabilize
-        // after first render to prevent infinite render phase updates.
-        ignorePreviousDependencies = false;
-      }
-
-      // Start over from the beginning of the list
-      currentHook = null;
-      workInProgressHook = null;
-
-      workInProgress.updateQueue = null;
-
-      if (__DEV__) {
-        // Also validate hook order for cascading updates.
-        hookTypesUpdateIndexDev = -1;
-      }
-
-      ReactCurrentDispatcher.current = __DEV__
-        ? HooksDispatcherOnRerenderInDEV
-        : HooksDispatcherOnRerender;
+    // Keep rendering until the component stabilizes (there are no more render
+    // phase updates).
+    children = renderWithHooksAgain(
+      workInProgress,
+      Component,
+      props,
+      secondArg,
+      prevThenableState,
+    );
+  }
 
-      prepareThenableState(prevThenableState);
-      children = Component(props, secondArg);
-    } while (didScheduleRenderPhaseUpdateDuringThisPass);
+  if (shouldDoubleRenderDEV) {
+    // In development, components are invoked twice to help detect side effects.
+    setIsStrictModeForDevtools(true);
+    try {
+      children = renderWithHooksAgain(
+        workInProgress,
+        Component,
+        props,
+        secondArg,
+        prevThenableState,
+      );
+    } finally {
+      setIsStrictModeForDevtools(false);
+    }
   }
 
   // We can assume the previous dispatcher is always this one, since we set it
@@ -616,6 +642,65 @@ export function renderWithHooks<Props, SecondArg>(
   return children;
 }
 
+function renderWithHooksAgain<Props, SecondArg>(
+  workInProgress: Fiber,
+  Component: (p: Props, arg: SecondArg) => any,
+  props: Props,
+  secondArg: SecondArg,
+  prevThenableState: ThenableState | null,
+) {
+  // This is used to perform another render pass. It's used when setState is
+  // called during render, and for double invoking components in Strict Mode
+  // during development.
+  //
+  // The state from the previous pass is reused whenever possible. So, state
+  // updates that were already processed are not processed again, and memoized
+  // functions (`useMemo`) are not invoked again.
+  //
+  // Keep rendering in a loop for as long as render phase updates continue to
+  // be scheduled. Use a counter to prevent infinite loops.
+  let numberOfReRenders: number = 0;
+  let children;
+  do {
+    didScheduleRenderPhaseUpdateDuringThisPass = false;
+    localIdCounter = 0;
+    thenableIndexCounter = 0;
+
+    if (numberOfReRenders >= RE_RENDER_LIMIT) {
+      throw new Error(
+        'Too many re-renders. React limits the number of renders to prevent ' +
+          'an infinite loop.',
+      );
+    }
+
+    numberOfReRenders += 1;
+    if (__DEV__) {
+      // Even when hot reloading, allow dependencies to stabilize
+      // after first render to prevent infinite render phase updates.
+      ignorePreviousDependencies = false;
+    }
+
+    // Start over from the beginning of the list
+    currentHook = null;
+    workInProgressHook = null;
+
+    workInProgress.updateQueue = null;
+
+    if (__DEV__) {
+      // Also validate hook order for cascading updates.
+      hookTypesUpdateIndexDev = -1;
+    }
+
+    ReactCurrentDispatcher.current = __DEV__
+      ? HooksDispatcherOnRerenderInDEV
+      : HooksDispatcherOnRerender;
+
+    prepareThenableState(prevThenableState);
+    children = Component(props, secondArg);
+  } while (didScheduleRenderPhaseUpdateDuringThisPass);
+  return children;
+}
+
 export function checkDidRenderIdHook(): boolean {
   // This should be called immediately after every renderWithHooks call.
   // Conceptually, it's part of the return value of renderWithHooks; it's only a
@@ -1023,12 +1108,15 @@ function updateReducer<S, I, A>(
         }
 
         // Process this update.
+        const action = update.action;
+        if (shouldDoubleInvokeUserFnsInHooksDEV) {
+          reducer(newState, action);
+        }
         if (update.hasEagerState) {
           // If this update is a state update (not a reducer) and was processed eagerly,
           // we can use the eagerly computed state
           newState = ((update.eagerState: any): S);
         } else {
-          const action = update.action;
           newState = reducer(newState, action);
         }
       }
@@ -2110,6 +2198,9 @@ function mountMemo<T>(
 ): T {
   const hook = mountWorkInProgressHook();
   const nextDeps = deps === undefined ? null : deps;
+  if (shouldDoubleInvokeUserFnsInHooksDEV) {
+    nextCreate();
+  }
   const nextValue = nextCreate();
   hook.memoizedState = [nextValue, nextDeps];
   return nextValue;
@@ -2131,6 +2222,9 @@ function updateMemo<T>(
       }
     }
   }
+  if (shouldDoubleInvokeUserFnsInHooksDEV) {
+    nextCreate();
+  }
   const nextValue = nextCreate();
   hook.memoizedState = [nextValue, nextDeps];
   return nextValue;

filename: packages/react/src/__tests__/ReactStrictMode-test.js
patch: @@ -15,6 +15,10 @@ let ReactDOMClient;
 let ReactDOMServer;
 let Scheduler;
 let PropTypes;
+let act;
+let useMemo;
+let useState;
+let useReducer;
 
 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
 
@@ -25,6 +29,10 @@ describe('ReactStrictMode', () => {
     ReactDOM = require('react-dom');
     ReactDOMClient = require('react-dom/client');
     ReactDOMServer = require('react-dom/server');
+    act = require('jest-react').act;
+    useMemo = React.useMemo;
+    useState = React.useState;
+    useReducer = React.useReducer;
   });
 
   it('should appear in the client component stack', () => {
@@ -331,6 +339,183 @@ describe('ReactStrictMode', () => {
     // But each time `state` should be the previous value
     expect(instance.state.count).toBe(2);
   });
+
+  // @gate debugRenderPhaseSideEffectsForStrictMode
+  it('double invokes useMemo functions', async () => {
+    let log = [];
+
+    function Uppercased({text}) {
+      return useMemo(() => {
+        const uppercased = text.toUpperCase();
+        log.push('Compute toUpperCase: ' + uppercased);
+        return uppercased;
+      }, [text]);
+    }
+
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+
+    // Mount
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <Uppercased text="hello" />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('HELLO');
+    expect(log).toEqual([
+      'Compute toUpperCase: HELLO',
+      'Compute toUpperCase: HELLO',
+    ]);
+
+    log = [];
+
+    // Update
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <Uppercased text="goodbye" />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('GOODBYE');
+    expect(log).toEqual([
+      'Compute toUpperCase: GOODBYE',
+      'Compute toUpperCase: GOODBYE',
+    ]);
+  });
+
+  // @gate debugRenderPhaseSideEffectsForStrictMode
+  it('double invokes useMemo functions', async () => {
+    let log = [];
+    function Uppercased({text}) {
+      const memoizedResult = useMemo(() => {
+        const uppercased = text.toUpperCase();
+        log.push('Compute toUpperCase: ' + uppercased);
+        return {uppercased};
+      }, [text]);
+
+      // Push this to the log so we can check whether the same memoized result
+      // it returned during both invocations.
+      log.push(memoizedResult);
+
+      return memoizedResult.uppercased;
+    }
+
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+
+    // Mount
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <Uppercased text="hello" />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('HELLO');
+    expect(log).toEqual([
+      'Compute toUpperCase: HELLO',
+      'Compute toUpperCase: HELLO',
+      {uppercased: 'HELLO'},
+      {uppercased: 'HELLO'},
+    ]);
+
+    // Even though the memoized function is invoked twice, the same object
+    // is returned both times.
+    expect(log[2]).toBe(log[3]);
+
+    log = [];
+
+    // Update
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <Uppercased text="goodbye" />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('GOODBYE');
+    expect(log).toEqual([
+      'Compute toUpperCase: GOODBYE',
+      'Compute toUpperCase: GOODBYE',
+      {uppercased: 'GOODBYE'},
+      {uppercased: 'GOODBYE'},
+    ]);
+
+    // Even though the memoized function is invoked twice, the same object
+    // is returned both times.
+    expect(log[2]).toBe(log[3]);
+  });
+
+  // @gate debugRenderPhaseSideEffectsForStrictMode
+  it('double invokes setState updater functions', async () => {
+    const log = [];
+
+    let setCount;
+    function App() {
+      const [count, _setCount] = useState(0);
+      setCount = _setCount;
+      return count;
+    }
+
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <App />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('0');
+
+    await act(() => {
+      setCount(() => {
+        log.push('Compute count: 1');
+        return 1;
+      });
+    });
+    expect(container.textContent).toBe('1');
+    expect(log).toEqual(['Compute count: 1', 'Compute count: 1']);
+  });
+
+  // @gate debugRenderPhaseSideEffectsForStrictMode
+  it('double invokes reducer functions', async () => {
+    const log = [];
+
+    function reducer(prevState, action) {
+      log.push('Compute new state: ' + action);
+      return action;
+    }
+
+    let dispatch;
+    function App() {
+      const [count, _dispatch] = useReducer(reducer, 0);
+      dispatch = _dispatch;
+      return count;
+    }
+
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+
+    await act(() => {
+      root.render(
+        <React.StrictMode>
+          <App />
+        </React.StrictMode>,
+      );
+    });
+    expect(container.textContent).toBe('0');
+
+    await act(() => {
+      dispatch(1);
+    });
+    expect(container.textContent).toBe('1');
+    expect(log).toEqual(['Compute new state: 1', 'Compute new state: 1']);
+  });
 });
 
 describe('Concurrent Mode', () => {