[bug] Fix component name for Portal and add tests (#32640)
Based off: https://github.com/facebook/react/pull/32499 While looking into `React.lazy` issues for built-ins, I noticed we already error for `lazy` with build-ins, but we don't have any tests for `getComponentNameFromType` using all the built-ins. This may be something we should handle, but for now we should at least have tests. Here's why: while writing tests, I noticed we check `type` instead of `$$typeof` for portals: https://github.com/facebook/react/blob/9cdf8a99edcfd94d7420835ea663edca04237527/packages/react-reconciler/src/ReactPortal.js#L25-L32 This PR adds tests for all the built-ins and fixes the portal bug. [Commit to review](https://github.com/facebook/react/pull/32640/commits/e068c167d48d4df01e79db8f13276bb46d7ab439)
Ricky committed
Mar 17, 2025 at 11:23 UTC
8243f3f0631698e819c690710a7f18f767068981
2 files changed
+277
-3
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+275
-1
@@ -791,7 +791,7 @@ describe('ReactLazy', () => {
791
);
792
});
793
794
- it('throws with a useful error when wrapping fragment with lazy()', async () => {
794
+ it('throws with a useful error when wrapping Fragment with lazy()', async () => {
795
const BadLazy = lazy(() => fakeImport(React.Fragment));
796
797
const root = ReactTestRenderer.create(
@@ -817,6 +817,280 @@ describe('ReactLazy', () => {
817
);
818
});
819
820
+ // @gate !fb
821
+ it('throws with a useful error when wrapping createPortal with lazy()', async () => {
822
+ const ReactDOM = require('react-dom');
823
+ const container = document.createElement('div');
824
+ const portal = ReactDOM.createPortal(<div />, container);
825
+ const BadLazy = lazy(() => fakeImport(portal));
826
+
827
+ const root = ReactTestRenderer.create(
828
+ <Suspense fallback={<Text text="Loading..." />}>
829
+ <BadLazy />
830
+ </Suspense>,
831
+ {
832
+ unstable_isConcurrent: true,
833
+ },
834
+ );
835
+
836
+ await waitForAll(['Loading...']);
837
+
838
+ await resolveFakeImport(portal);
839
+ root.update(
840
+ <Suspense fallback={<Text text="Loading..." />}>
841
+ <BadLazy />
842
+ </Suspense>,
843
+ );
844
+ await waitForThrow(
845
+ 'Element type is invalid. Received a promise that resolves to: Portal. ' +
846
+ 'Lazy element type must resolve to a class or function.',
847
+ );
848
+ });
849
+
850
+ it('throws with a useful error when wrapping Profiler with lazy()', async () => {
851
+ const BadLazy = lazy(() => fakeImport(React.Profiler));
852
+
853
+ const root = ReactTestRenderer.create(
854
+ <Suspense fallback={<Text text="Loading..." />}>
855
+ <BadLazy />
856
+ </Suspense>,
857
+ {
858
+ unstable_isConcurrent: true,
859
+ },
860
+ );
861
+
862
+ await waitForAll(['Loading...']);
863
+
864
+ await resolveFakeImport(React.Profiler);
865
+ root.update(
866
+ <Suspense fallback={<Text text="Loading..." />}>
867
+ <BadLazy />
868
+ </Suspense>,
869
+ );
870
+ await waitForThrow(
871
+ 'Element type is invalid. Received a promise that resolves to: Profiler. ' +
872
+ 'Lazy element type must resolve to a class or function.',
873
+ );
874
+ });
875
+
876
+ it('throws with a useful error when wrapping StrictMode with lazy()', async () => {
877
+ const BadLazy = lazy(() => fakeImport(React.StrictMode));
878
+
879
+ const root = ReactTestRenderer.create(
880
+ <Suspense fallback={<Text text="Loading..." />}>
881
+ <BadLazy />
882
+ </Suspense>,
883
+ {
884
+ unstable_isConcurrent: true,
885
+ },
886
+ );
887
+
888
+ await waitForAll(['Loading...']);
889
+
890
+ await resolveFakeImport(React.StrictMode);
891
+ root.update(
892
+ <Suspense fallback={<Text text="Loading..." />}>
893
+ <BadLazy />
894
+ </Suspense>,
895
+ );
896
+ await waitForThrow(
897
+ 'Element type is invalid. Received a promise that resolves to: StrictMode. ' +
898
+ 'Lazy element type must resolve to a class or function.',
899
+ );
900
+ });
901
+
902
+ it('throws with a useful error when wrapping Suspense with lazy()', async () => {
903
+ const BadLazy = lazy(() => fakeImport(React.Suspense));
904
+
905
+ const root = ReactTestRenderer.create(
906
+ <Suspense fallback={<Text text="Loading..." />}>
907
+ <BadLazy />
908
+ </Suspense>,
909
+ {
910
+ unstable_isConcurrent: true,
911
+ },
912
+ );
913
+
914
+ await waitForAll(['Loading...']);
915
+
916
+ await resolveFakeImport(React.Suspense);
917
+ root.update(
918
+ <Suspense fallback={<Text text="Loading..." />}>
919
+ <BadLazy />
920
+ </Suspense>,
921
+ );
922
+ await waitForThrow(
923
+ 'Element type is invalid. Received a promise that resolves to: Suspense. ' +
924
+ 'Lazy element type must resolve to a class or function.',
925
+ );
926
+ });
927
+
928
+ it('throws with a useful error when wrapping Context with lazy()', async () => {
929
+ const Context = React.createContext(null);
930
+ const BadLazy = lazy(() => fakeImport(Context));
931
+
932
+ const root = ReactTestRenderer.create(
933
+ <Suspense fallback={<Text text="Loading..." />}>
934
+ <BadLazy />
935
+ </Suspense>,
936
+ {
937
+ unstable_isConcurrent: true,
938
+ },
939
+ );
940
+
941
+ await waitForAll(['Loading...']);
942
+
943
+ await resolveFakeImport(Context);
944
+ root.update(
945
+ <Suspense fallback={<Text text="Loading..." />}>
946
+ <BadLazy />
947
+ </Suspense>,
948
+ );
949
+ await waitForThrow(
950
+ gate('enableRenderableContext')
951
+ ? 'Element type is invalid. Received a promise that resolves to: Context.Provider. ' +
952
+ 'Lazy element type must resolve to a class or function.'
953
+ : 'Element type is invalid. Received a promise that resolves to: Context.Consumer. ' +
954
+ 'Lazy element type must resolve to a class or function.',
955
+ );
956
+ });
957
+
958
+ // @gate enableRenderableContext
959
+ it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => {
960
+ const Context = React.createContext(null);
961
+ const BadLazy = lazy(() => fakeImport(Context.Consumer));
962
+
963
+ const root = ReactTestRenderer.create(
964
+ <Suspense fallback={<Text text="Loading..." />}>
965
+ <BadLazy />
966
+ </Suspense>,
967
+ {
968
+ unstable_isConcurrent: true,
969
+ },
970
+ );
971
+
972
+ await waitForAll(['Loading...']);
973
+
974
+ await resolveFakeImport(Context.Consumer);
975
+ root.update(
976
+ <Suspense fallback={<Text text="Loading..." />}>
977
+ <BadLazy />
978
+ </Suspense>,
979
+ );
980
+ await waitForThrow(
981
+ 'Element type is invalid. Received a promise that resolves to: Context.Consumer. ' +
982
+ 'Lazy element type must resolve to a class or function.',
983
+ );
984
+ });
985
+
986
+ // @gate enableSuspenseList
987
+ it('throws with a useful error when wrapping SuspenseList with lazy()', async () => {
988
+ const BadLazy = lazy(() => fakeImport(React.unstable_SuspenseList));
989
+
990
+ const root = ReactTestRenderer.create(
991
+ <Suspense fallback={<Text text="Loading..." />}>
992
+ <BadLazy />
993
+ </Suspense>,
994
+ {
995
+ unstable_isConcurrent: true,
996
+ },
997
+ );
998
+
999
+ await waitForAll(['Loading...']);
1000
+
1001
+ await resolveFakeImport(React.unstable_SuspenseList);
1002
+ root.update(
1003
+ <Suspense fallback={<Text text="Loading..." />}>
1004
+ <BadLazy />
1005
+ </Suspense>,
1006
+ );
1007
+ await waitForThrow(
1008
+ 'Element type is invalid. Received a promise that resolves to: SuspenseList. ' +
1009
+ 'Lazy element type must resolve to a class or function.',
1010
+ );
1011
+ });
1012
+
1013
+ // @gate enableViewTransition
1014
+ it('throws with a useful error when wrapping ViewTransition with lazy()', async () => {
1015
+ const BadLazy = lazy(() => fakeImport(React.unstable_ViewTransition));
1016
+
1017
+ const root = ReactTestRenderer.create(
1018
+ <Suspense fallback={<Text text="Loading..." />}>
1019
+ <BadLazy />
1020
+ </Suspense>,
1021
+ {
1022
+ unstable_isConcurrent: true,
1023
+ },
1024
+ );
1025
+
1026
+ await waitForAll(['Loading...']);
1027
+
1028
+ await resolveFakeImport(React.unstable_ViewTransition);
1029
+ root.update(
1030
+ <Suspense fallback={<Text text="Loading..." />}>
1031
+ <BadLazy />
1032
+ </Suspense>,
1033
+ );
1034
+ await waitForThrow(
1035
+ 'Element type is invalid. Received a promise that resolves to: ViewTransition. ' +
1036
+ 'Lazy element type must resolve to a class or function.',
1037
+ );
1038
+ });
1039
+
1040
+ // @gate enableActivity
1041
+ it('throws with a useful error when wrapping Activity with lazy()', async () => {
1042
+ const BadLazy = lazy(() => fakeImport(React.unstable_Activity));
1043
+
1044
+ const root = ReactTestRenderer.create(
1045
+ <Suspense fallback={<Text text="Loading..." />}>
1046
+ <BadLazy />
1047
+ </Suspense>,
1048
+ {
1049
+ unstable_isConcurrent: true,
1050
+ },
1051
+ );
1052
+
1053
+ await waitForAll(['Loading...']);
1054
+
1055
+ await resolveFakeImport(React.unstable_Activity);
1056
+ root.update(
1057
+ <Suspense fallback={<Text text="Loading..." />}>
1058
+ <BadLazy />
1059
+ </Suspense>,
1060
+ );
1061
+ await waitForThrow(
1062
+ 'Element type is invalid. Received a promise that resolves to: Activity. ' +
1063
+ 'Lazy element type must resolve to a class or function.',
1064
+ );
1065
+ });
1066
+
1067
+ // @gate enableTransitionTracing
1068
+ it('throws with a useful error when wrapping TracingMarker with lazy()', async () => {
1069
+ const BadLazy = lazy(() => fakeImport(React.unstable_TracingMarker));
1070
+
1071
+ const root = ReactTestRenderer.create(
1072
+ <Suspense fallback={<Text text="Loading..." />}>
1073
+ <BadLazy />
1074
+ </Suspense>,
1075
+ {
1076
+ unstable_isConcurrent: true,
1077
+ },
1078
+ );
1079
+
1080
+ await waitForAll(['Loading...']);
1081
+
1082
+ await resolveFakeImport(React.unstable_TracingMarker);
1083
+ root.update(
1084
+ <Suspense fallback={<Text text="Loading..." />}>
1085
+ <BadLazy />
1086
+ </Suspense>,
1087
+ );
1088
+ await waitForThrow(
1089
+ 'Element type is invalid. Received a promise that resolves to: TracingMarker. ' +
1090
+ 'Lazy element type must resolve to a class or function.',
1091
+ );
1092
+ });
1093
+
1094
it('throws with a useful error when wrapping lazy() multiple times', async () => {
1095
const Lazy1 = lazy(() => fakeImport(Text));
1096
const Lazy2 = lazy(() => fakeImport(Lazy1));
packages/shared/getComponentNameFromType.js
+2
-2
@@ -74,8 +74,6 @@ export default function getComponentNameFromType(type: mixed): string | null {
74
switch (type) {
75
case REACT_FRAGMENT_TYPE:
76
return 'Fragment';
77
- case REACT_PORTAL_TYPE:
78
- return 'Portal';
77
case REACT_PROFILER_TYPE:
78
return 'Profiler';
79
case REACT_STRICT_MODE_TYPE:
@@ -106,6 +104,8 @@ export default function getComponentNameFromType(type: mixed): string | null {
104
}
105
}
106
switch (type.$$typeof) {
107
+ case REACT_PORTAL_TYPE:
108
+ return 'Portal';
109
case REACT_PROVIDER_TYPE:
110
if (enableRenderableContext) {
111
return null;