Skip to content

Commit bf7f4cf

Browse files
authored
feat: support more element types (#617)
* Add a failing test for printing `forwardRef` element * Unwind `getReactElementDisplayName` to a cascade of if statements * Refactor `getReactElementDisplayName` to switch statement * Support stringifying more element types * fix: flow types * chore: adjust fallback case in the `getReactElementDisplayName`
1 parent 6b59bd5 commit bf7f4cf

File tree

5 files changed

+246
-59
lines changed

5 files changed

+246
-59
lines changed

package.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424
"smoke": "node tests/smoke/run"
2525
},
2626
"lint-staged": {
27-
"*.js": [
28-
"prettier --write \"**/*.{js,json}\"",
29-
"git add"
30-
]
27+
"*.js": ["prettier --write \"**/*.{js,json}\"", "git add"]
3128
},
3229
"author": {
3330
"name": "Algolia, Inc.",
@@ -72,7 +69,6 @@
7269
"react-test-renderer": "16.14.0",
7370
"rollup": "1.32.1",
7471
"rollup-plugin-babel": "4.4.0",
75-
"rollup-plugin-commonjs": "10.1.0",
7672
"rollup-plugin-node-builtins": "2.1.2",
7773
"rollup-plugin-node-globals": "1.4.0",
7874
"rollup-plugin-node-resolve": "5.2.0",
@@ -84,11 +80,10 @@
8480
},
8581
"dependencies": {
8682
"@base2/pretty-print-object": "1.0.0",
87-
"is-plain-object": "3.0.1"
83+
"is-plain-object": "3.0.1",
84+
"react-is": "^17.0.2"
8885
},
8986
"jest": {
90-
"setupFilesAfterEnv": [
91-
"<rootDir>tests/setupTests.js"
92-
]
87+
"setupFilesAfterEnv": ["<rootDir>tests/setupTests.js"]
9388
}
9489
}

rollup.config.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import babel from 'rollup-plugin-babel';
2-
import commonjs from 'rollup-plugin-commonjs';
32
import resolve from 'rollup-plugin-node-resolve';
43
import builtins from 'rollup-plugin-node-builtins';
54
import globals from 'rollup-plugin-node-globals';
65
import pkg from './package.json';
76

8-
const extractPackagePeerDependencies = () =>
9-
Object.keys(pkg.peerDependencies) || [];
7+
const extractExternals = () => [
8+
...Object.keys(pkg.dependencies || {}),
9+
...Object.keys(pkg.peerDependencies || {}),
10+
];
1011

1112
export default {
1213
input: 'src/index.js',
@@ -22,7 +23,7 @@ export default {
2223
sourcemap: true,
2324
},
2425
],
25-
external: extractPackagePeerDependencies(),
26+
external: extractExternals(),
2627
plugins: [
2728
babel({
2829
babelrc: false,
@@ -36,12 +37,6 @@ export default {
3637
resolve({
3738
mainFields: ['module', 'main', 'jsnext', 'browser'],
3839
}),
39-
commonjs({
40-
sourceMap: true,
41-
namedExports: {
42-
react: ['isValidElement'],
43-
},
44-
}),
4540
globals(),
4641
builtins(),
4742
],

src/index.spec.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,4 +1160,174 @@ describe('reactElementToJSXString(ReactElement)', () => {
11601160
}
11611161
mount(<App />);
11621162
});
1163+
1164+
it('should use inferred function name as display name for `forwardRef` element', () => {
1165+
const Tag = React.forwardRef(function Tag({ text }, ref) {
1166+
return <span ref={ref}>{text}</span>;
1167+
});
1168+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1169+
`<Tag text="some label" />`
1170+
);
1171+
});
1172+
1173+
it('should use `displayName` instead of inferred function name as display name for `forwardRef` element', () => {
1174+
const Tag = React.forwardRef(function Tag({ text }, ref) {
1175+
return <span ref={ref}>{text}</span>;
1176+
});
1177+
Tag.displayName = 'MyTag';
1178+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1179+
`<MyTag text="some label" />`
1180+
);
1181+
});
1182+
1183+
it('should use inferred function name as display name for `memo` element', () => {
1184+
const Tag = React.memo(function Tag({ text }) {
1185+
return <span>{text}</span>;
1186+
});
1187+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1188+
`<Tag text="some label" />`
1189+
);
1190+
});
1191+
1192+
it('should use `displayName` instead of inferred function name as display name for `memo` element', () => {
1193+
const Tag = React.memo(function Tag({ text }) {
1194+
return <span>{text}</span>;
1195+
});
1196+
Tag.displayName = 'MyTag';
1197+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1198+
`<MyTag text="some label" />`
1199+
);
1200+
});
1201+
1202+
it('should use inferred function name as display name for a `forwardRef` wrapped in `memo`', () => {
1203+
const Tag = React.memo(
1204+
React.forwardRef(function Tag({ text }, ref) {
1205+
return <span ref={ref}>{text}</span>;
1206+
})
1207+
);
1208+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1209+
`<Tag text="some label" />`
1210+
);
1211+
});
1212+
1213+
it('should use inferred function name as display name for a component wrapped in `memo` multiple times', () => {
1214+
const Tag = React.memo(
1215+
React.memo(
1216+
React.memo(function Tag({ text }) {
1217+
return <span>{text}</span>;
1218+
})
1219+
)
1220+
);
1221+
expect(reactElementToJSXString(<Tag text="some label" />)).toEqual(
1222+
`<Tag text="some label" />`
1223+
);
1224+
});
1225+
1226+
it('should stringify `StrictMode` correctly', () => {
1227+
const App = () => null;
1228+
1229+
expect(
1230+
reactElementToJSXString(
1231+
<React.StrictMode>
1232+
<App />
1233+
</React.StrictMode>
1234+
)
1235+
).toEqual(`<StrictMode>
1236+
<App />
1237+
</StrictMode>`);
1238+
});
1239+
1240+
it('should stringify `Suspense` correctly', () => {
1241+
const Spinner = () => null;
1242+
const ProfilePage = () => null;
1243+
1244+
expect(
1245+
reactElementToJSXString(
1246+
<React.Suspense fallback={<Spinner />}>
1247+
<ProfilePage />
1248+
</React.Suspense>
1249+
)
1250+
).toEqual(`<Suspense fallback={<Spinner />}>
1251+
<ProfilePage />
1252+
</Suspense>`);
1253+
});
1254+
1255+
it('should stringify `Profiler` correctly', () => {
1256+
const Navigation = () => null;
1257+
1258+
expect(
1259+
reactElementToJSXString(
1260+
<React.Profiler id="Navigation" onRender={() => {}}>
1261+
<Navigation />
1262+
</React.Profiler>
1263+
)
1264+
).toEqual(`<Profiler
1265+
id="Navigation"
1266+
onRender={function noRefCheck() {}}
1267+
>
1268+
<Navigation />
1269+
</Profiler>`);
1270+
});
1271+
1272+
it('should stringify `Contex.Provider` correctly', () => {
1273+
const Ctx = React.createContext();
1274+
const App = () => {};
1275+
1276+
expect(
1277+
reactElementToJSXString(
1278+
<Ctx.Provider value={null}>
1279+
<App />
1280+
</Ctx.Provider>
1281+
)
1282+
).toEqual(`<Context.Provider value={null}>
1283+
<App />
1284+
</Context.Provider>`);
1285+
});
1286+
1287+
it('should stringify `Contex.Provider` with `displayName` correctly', () => {
1288+
const Ctx = React.createContext();
1289+
Ctx.displayName = 'MyCtx';
1290+
1291+
const App = () => {};
1292+
1293+
expect(
1294+
reactElementToJSXString(
1295+
<Ctx.Provider value={null}>
1296+
<App />
1297+
</Ctx.Provider>
1298+
)
1299+
).toEqual(`<MyCtx.Provider value={null}>
1300+
<App />
1301+
</MyCtx.Provider>`);
1302+
});
1303+
1304+
it('should stringify `Contex.Consumer` correctly', () => {
1305+
const Ctx = React.createContext();
1306+
const Button = () => null;
1307+
1308+
expect(
1309+
reactElementToJSXString(
1310+
<Ctx.Consumer>{theme => <Button theme={theme} />}</Ctx.Consumer>
1311+
)
1312+
).toEqual(`<Context.Consumer />`);
1313+
});
1314+
1315+
it('should stringify `Contex.Consumer` with `displayName` correctly', () => {
1316+
const Ctx = React.createContext();
1317+
Ctx.displayName = 'MyCtx';
1318+
1319+
const Button = () => null;
1320+
1321+
expect(
1322+
reactElementToJSXString(
1323+
<Ctx.Consumer>{theme => <Button theme={theme} />}</Ctx.Consumer>
1324+
)
1325+
).toEqual(`<MyCtx.Consumer />`);
1326+
});
1327+
1328+
it('should stringify `lazy` component correctly', () => {
1329+
const Lazy = React.lazy(() => Promise.resolve(() => {}));
1330+
1331+
expect(reactElementToJSXString(<Lazy />)).toEqual(`<Lazy />`);
1332+
});
11631333
});

src/parser/parseReactElement.js

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
/* @flow */
22

33
import React, { type Element as ReactElement, Fragment } from 'react';
4+
import {
5+
ForwardRef,
6+
isContextConsumer,
7+
isContextProvider,
8+
isForwardRef,
9+
isLazy,
10+
isMemo,
11+
isProfiler,
12+
isStrictMode,
13+
isSuspense,
14+
Memo,
15+
} from 'react-is';
416
import type { Options } from './../options';
517
import {
618
createStringTreeNode,
@@ -12,12 +24,56 @@ import type { TreeNode } from './../tree';
1224

1325
const supportFragment = Boolean(Fragment);
1426

15-
const getReactElementDisplayName = (element: ReactElement<*>): string =>
16-
element.type.displayName ||
17-
(element.type.name !== '_default' ? element.type.name : null) || // function name
18-
(typeof element.type === 'function' // function without a name, you should provide one
19-
? 'No Display Name'
20-
: element.type);
27+
const getFunctionTypeName = (functionType): string => {
28+
if (!functionType.name || functionType.name === '_default') {
29+
return 'No Display Name';
30+
}
31+
return functionType.name;
32+
};
33+
34+
const getWrappedComponentDisplayName = (Component: *): string => {
35+
switch (true) {
36+
case Boolean(Component.displayName):
37+
return Component.displayName;
38+
case Component.$$typeof === Memo:
39+
return getWrappedComponentDisplayName(Component.type);
40+
case Component.$$typeof === ForwardRef:
41+
return getWrappedComponentDisplayName(Component.render);
42+
default:
43+
return getFunctionTypeName(Component);
44+
}
45+
};
46+
47+
// heavily inspired by:
48+
// https://github.com/facebook/react/blob/3746eaf985dd92f8aa5f5658941d07b6b855e9d9/packages/react-devtools-shared/src/backend/renderer.js#L399-L496
49+
const getReactElementDisplayName = (element: ReactElement<*>): string => {
50+
switch (true) {
51+
case typeof element.type === 'string':
52+
return element.type;
53+
case typeof element.type === 'function':
54+
if (element.type.displayName) {
55+
return element.type.displayName;
56+
}
57+
return getFunctionTypeName(element.type);
58+
case isForwardRef(element):
59+
case isMemo(element):
60+
return getWrappedComponentDisplayName(element.type);
61+
case isContextConsumer(element):
62+
return `${element.type._context.displayName || 'Context'}.Consumer`;
63+
case isContextProvider(element):
64+
return `${element.type._context.displayName || 'Context'}.Provider`;
65+
case isLazy(element):
66+
return 'Lazy';
67+
case isProfiler(element):
68+
return 'Profiler';
69+
case isStrictMode(element):
70+
return 'StrictMode';
71+
case isSuspense(element):
72+
return 'Suspense';
73+
default:
74+
return 'UnknownElementType';
75+
}
76+
};
2177

2278
const noChildren = (propsValue, propName) => propName !== 'children';
2379

0 commit comments

Comments
 (0)