41
enableUseMemoCacheHook,
42
enableUseEventHook,
43
enableLegacyCache,
44
+ debugRenderPhaseSideEffectsForStrictMode,
45
} from 'shared/ReactFeatureFlags';
46
import {
47
REACT_CONTEXT_TYPE,
54
ConcurrentMode,
55
DebugTracingMode,
56
StrictEffectsMode,
57
+ StrictLegacyMode,
58
} from './ReactTypeOfMode';
59
import {
60
NoLane,
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,
145
trackUsedThenable,
146
checkIfUseWrappedInTryCatch,
147
} from './ReactFiberThenable.new';
148
+import type {ThenableState} from './ReactFiberThenable.new';
149
150
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
151
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
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
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
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
}
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;
2222
}
2223
}
2224
}
2225
+ if (shouldDoubleInvokeUserFnsInHooksDEV) {
2226
+ nextCreate();
2227
+ }
2228
const nextValue = nextCreate();
2229
hook.memoizedState = [nextValue, nextDeps];
2230
return nextValue;