Skip to content

Commit a1414e8

Browse files
authored
Double-render function components with Hooks in DEV in StrictMode (#14643)
* Double-render functions in strict mode * Double-invoke first function component render too * Mark TestRendererAsync test as internal and revert changes to it TestRenderer is built with strict mode doublerender off. We could change that but I'm not sure we want to. So I'll just flip the flag off for this test. * Only double-invoke components using Hooks * Revert unintentional change
1 parent 10a7a5b commit a1414e8

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed

Diff for: packages/react-reconciler/src/ReactFiberBeginWork.js

+53
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,23 @@ function updateForwardRef(
250250
ref,
251251
renderExpirationTime,
252252
);
253+
if (
254+
debugRenderPhaseSideEffects ||
255+
(debugRenderPhaseSideEffectsForStrictMode &&
256+
workInProgress.mode & StrictMode)
257+
) {
258+
// Only double-render components with Hooks
259+
if (workInProgress.memoizedState !== null) {
260+
renderWithHooks(
261+
current,
262+
workInProgress,
263+
render,
264+
nextProps,
265+
ref,
266+
renderExpirationTime,
267+
);
268+
}
269+
}
253270
setCurrentPhase(null);
254271
} else {
255272
nextChildren = renderWithHooks(
@@ -543,6 +560,23 @@ function updateFunctionComponent(
543560
context,
544561
renderExpirationTime,
545562
);
563+
if (
564+
debugRenderPhaseSideEffects ||
565+
(debugRenderPhaseSideEffectsForStrictMode &&
566+
workInProgress.mode & StrictMode)
567+
) {
568+
// Only double-render components with Hooks
569+
if (workInProgress.memoizedState !== null) {
570+
renderWithHooks(
571+
current,
572+
workInProgress,
573+
Component,
574+
nextProps,
575+
context,
576+
renderExpirationTime,
577+
);
578+
}
579+
}
546580
setCurrentPhase(null);
547581
} else {
548582
nextChildren = renderWithHooks(
@@ -1207,6 +1241,25 @@ function mountIndeterminateComponent(
12071241
} else {
12081242
// Proceed under the assumption that this is a function component
12091243
workInProgress.tag = FunctionComponent;
1244+
if (__DEV__) {
1245+
if (
1246+
debugRenderPhaseSideEffects ||
1247+
(debugRenderPhaseSideEffectsForStrictMode &&
1248+
workInProgress.mode & StrictMode)
1249+
) {
1250+
// Only double-render components with Hooks
1251+
if (workInProgress.memoizedState !== null) {
1252+
renderWithHooks(
1253+
null,
1254+
workInProgress,
1255+
Component,
1256+
props,
1257+
context,
1258+
renderExpirationTime,
1259+
);
1260+
}
1261+
}
1262+
}
12101263
reconcileChildren(null, workInProgress, value, renderExpirationTime);
12111264
if (__DEV__) {
12121265
validateFunctionComponentInDev(workInProgress, Component);

Diff for: packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

+220
Original file line numberDiff line numberDiff line change
@@ -660,4 +660,224 @@ describe('ReactHooks', () => {
660660
'Hooks can only be called inside the body of a function component',
661661
);
662662
});
663+
664+
it('double-invokes components with Hooks in Strict Mode', () => {
665+
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true;
666+
667+
const {useState, StrictMode} = React;
668+
let renderCount = 0;
669+
670+
function NoHooks() {
671+
renderCount++;
672+
return <div />;
673+
}
674+
675+
function HasHooks() {
676+
useState(0);
677+
renderCount++;
678+
return <div />;
679+
}
680+
681+
const FwdRef = React.forwardRef((props, ref) => {
682+
renderCount++;
683+
return <div />;
684+
});
685+
686+
const FwdRefHasHooks = React.forwardRef((props, ref) => {
687+
useState(0);
688+
renderCount++;
689+
return <div />;
690+
});
691+
692+
const Memo = React.memo(props => {
693+
renderCount++;
694+
return <div />;
695+
});
696+
697+
const MemoHasHooks = React.memo(props => {
698+
useState(0);
699+
renderCount++;
700+
return <div />;
701+
});
702+
703+
function Factory() {
704+
return {
705+
state: {},
706+
render() {
707+
renderCount++;
708+
return <div />;
709+
},
710+
};
711+
}
712+
713+
let renderer = ReactTestRenderer.create(null);
714+
715+
renderCount = 0;
716+
renderer.update(<NoHooks />);
717+
expect(renderCount).toBe(1);
718+
renderCount = 0;
719+
renderer.update(<NoHooks />);
720+
expect(renderCount).toBe(1);
721+
renderCount = 0;
722+
renderer.update(
723+
<StrictMode>
724+
<NoHooks />
725+
</StrictMode>,
726+
);
727+
expect(renderCount).toBe(1);
728+
renderCount = 0;
729+
renderer.update(
730+
<StrictMode>
731+
<NoHooks />
732+
</StrictMode>,
733+
);
734+
expect(renderCount).toBe(1);
735+
736+
renderCount = 0;
737+
renderer.update(<FwdRef />);
738+
expect(renderCount).toBe(1);
739+
renderCount = 0;
740+
renderer.update(<FwdRef />);
741+
expect(renderCount).toBe(1);
742+
renderCount = 0;
743+
renderer.update(
744+
<StrictMode>
745+
<FwdRef />
746+
</StrictMode>,
747+
);
748+
expect(renderCount).toBe(1);
749+
renderCount = 0;
750+
renderer.update(
751+
<StrictMode>
752+
<FwdRef />
753+
</StrictMode>,
754+
);
755+
expect(renderCount).toBe(1);
756+
757+
renderCount = 0;
758+
renderer.update(<Memo arg={1} />);
759+
expect(renderCount).toBe(1);
760+
renderCount = 0;
761+
renderer.update(<Memo arg={2} />);
762+
expect(renderCount).toBe(1);
763+
renderCount = 0;
764+
renderer.update(
765+
<StrictMode>
766+
<Memo arg={1} />
767+
</StrictMode>,
768+
);
769+
expect(renderCount).toBe(1);
770+
renderCount = 0;
771+
renderer.update(
772+
<StrictMode>
773+
<Memo arg={2} />
774+
</StrictMode>,
775+
);
776+
expect(renderCount).toBe(1);
777+
778+
renderCount = 0;
779+
renderer.update(<Factory />);
780+
expect(renderCount).toBe(1);
781+
renderCount = 0;
782+
renderer.update(<Factory />);
783+
expect(renderCount).toBe(1);
784+
renderCount = 0;
785+
renderer.update(
786+
<StrictMode>
787+
<Factory />
788+
</StrictMode>,
789+
);
790+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
791+
renderCount = 0;
792+
renderer.update(
793+
<StrictMode>
794+
<Factory />
795+
</StrictMode>,
796+
);
797+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
798+
799+
renderCount = 0;
800+
renderer.update(<HasHooks />);
801+
expect(renderCount).toBe(1);
802+
renderCount = 0;
803+
renderer.update(<HasHooks />);
804+
expect(renderCount).toBe(1);
805+
renderCount = 0;
806+
renderer.update(
807+
<StrictMode>
808+
<HasHooks />
809+
</StrictMode>,
810+
);
811+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
812+
renderCount = 0;
813+
renderer.update(
814+
<StrictMode>
815+
<HasHooks />
816+
</StrictMode>,
817+
);
818+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
819+
820+
renderCount = 0;
821+
renderer.update(<FwdRefHasHooks />);
822+
expect(renderCount).toBe(1);
823+
renderCount = 0;
824+
renderer.update(<FwdRefHasHooks />);
825+
expect(renderCount).toBe(1);
826+
renderCount = 0;
827+
renderer.update(
828+
<StrictMode>
829+
<FwdRefHasHooks />
830+
</StrictMode>,
831+
);
832+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
833+
renderCount = 0;
834+
renderer.update(
835+
<StrictMode>
836+
<FwdRefHasHooks />
837+
</StrictMode>,
838+
);
839+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
840+
841+
renderCount = 0;
842+
renderer.update(<MemoHasHooks arg={1} />);
843+
expect(renderCount).toBe(1);
844+
renderCount = 0;
845+
renderer.update(<MemoHasHooks arg={2} />);
846+
expect(renderCount).toBe(1);
847+
renderCount = 0;
848+
renderer.update(
849+
<StrictMode>
850+
<MemoHasHooks arg={1} />
851+
</StrictMode>,
852+
);
853+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
854+
renderCount = 0;
855+
renderer.update(
856+
<StrictMode>
857+
<MemoHasHooks arg={2} />
858+
</StrictMode>,
859+
);
860+
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
861+
});
862+
863+
it('double-invokes useMemo in DEV StrictMode despite []', () => {
864+
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true;
865+
const {useMemo, StrictMode} = React;
866+
867+
let useMemoCount = 0;
868+
function BadUseMemo() {
869+
useMemo(() => {
870+
useMemoCount++;
871+
}, []);
872+
return <div />;
873+
}
874+
875+
useMemoCount = 0;
876+
ReactTestRenderer.create(
877+
<StrictMode>
878+
<BadUseMemo />
879+
</StrictMode>,
880+
);
881+
expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
882+
});
663883
});

0 commit comments

Comments
 (0)