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
Dan Abramov committed
Jan 21, 2019 at 15:35 UTC
a1414e8949915c006bba411fa2949b304dd044ae
2 files changed
+273
packages/react-reconciler/src/ReactFiberBeginWork.js
+53
@@ -250,6 +250,23 @@ function updateForwardRef(
250
ref,
251
renderExpirationTime,
252
);
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
+ }
270
setCurrentPhase(null);
271
} else {
272
nextChildren = renderWithHooks(
@@ -543,6 +560,23 @@ function updateFunctionComponent(
560
context,
561
renderExpirationTime,
562
);
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
+ }
580
setCurrentPhase(null);
581
} else {
582
nextChildren = renderWithHooks(
@@ -1207,6 +1241,25 @@ function mountIndeterminateComponent(
1241
} else {
1242
// Proceed under the assumption that this is a function component
1243
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
+ }
1263
reconcileChildren(null, workInProgress, value, renderExpirationTime);
1264
if (__DEV__) {
1265
validateFunctionComponentInDev(workInProgress, Component);
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+220
@@ -660,4 +660,224 @@ describe('ReactHooks', () => {
660
'Hooks can only be called inside the body of a function component',
661
);
662
});
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
+ });
883
});