Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace memoized/forwarded raw components as prop values #682

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
"smoke": "node tests/smoke/run"
},
"lint-staged": {
"*.js": [
"prettier --write \"**/*.{js,json}\"",
"git add"
]
"*.js": ["prettier --write \"**/*.{js,json}\"", "git add"]
},
"author": {
"name": "Algolia, Inc.",
Expand Down Expand Up @@ -87,8 +84,6 @@
"react-is": "18.2.0"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>tests/setupTests.js"
]
"setupFilesAfterEnv": ["<rootDir>tests/setupTests.js"]
}
}
10 changes: 8 additions & 2 deletions src/formatter/formatPropValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { isPlainObject } from 'is-plain-object';
import { isValidElement } from 'react';
import type { Options } from './../options';
import parseReactElement from './../parser/parseReactElement';
import formatComplexDataStructure from './formatComplexDataStructure';
import formatFunction from './formatFunction';
import formatTreeNode from './formatTreeNode';
import type { Options } from './../options';
import parseReactElement from './../parser/parseReactElement';
import getWrappedComponentDisplayName from './getWrappedComponentDisplayName';

const escape = (s: string): string => s.replace(/"/g, '&quot;');

Expand Down Expand Up @@ -53,6 +54,11 @@ const formatPropValue = (
)}}`;
}

// handle memo & forwardRef
if (isPlainObject(propValue) && propValue.$$typeof) {
quantizor marked this conversation as resolved.
Show resolved Hide resolved
return `{${getWrappedComponentDisplayName(propValue)}}`;
}

if (propValue instanceof Date) {
if (isNaN(propValue.valueOf())) {
return `{new Date(NaN)}`;
Expand Down
38 changes: 38 additions & 0 deletions src/formatter/formatPropValue.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,42 @@ describe('formatPropValue', () => {

expect(formatPropValue(new Map(), false, 0, {})).toBe('{[object Map]}');
});

it('should format a memoized React component prop value', () => {
quantizor marked this conversation as resolved.
Show resolved Hide resolved
const Component = React.memo(function Foo() {
return <div />;
});

expect(formatPropValue(Component, false, 0, {})).toBe('{Foo}');

const Unnamed = React.memo(function() {
return <div />;
});

expect(formatPropValue(Unnamed, false, 0, {})).toBe('{Component}');
});

it('should format a forwarded React component prop value', () => {
const Component = React.forwardRef(function Foo(props, forwardedRef) {
return <div {...props} ref={forwardedRef} />;
});

expect(formatPropValue(Component, false, 0, {})).toBe('{Foo}');

const Unnamed = React.forwardRef(function(props, forwardedRef) {
return <div {...props} ref={forwardedRef} />;
});

expect(formatPropValue(Unnamed, false, 0, {})).toBe('{Component}');
});

it('should format a memoized & forwarded React component prop value', () => {
const Component = React.memo(
React.forwardRef(function Foo(props, forwardedRef) {
return <div {...props} ref={forwardedRef} />;
})
);

expect(formatPropValue(Component, false, 0, {})).toBe('{Foo}');
});
});
58 changes: 58 additions & 0 deletions src/formatter/formatReactPortalNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* @flow */

import type { Key } from 'react';
import formatReactElementNode from './formatReactElementNode';
import type { Options } from './../options';
import type {
ReactElementTreeNode,
ReactPortalTreeNode,
TreeNode,
} from './../tree';
import spacer from './spacer';

const toReactElementTreeNode = (
displayName: string,
key: ?Key,
childrens: TreeNode[]
): ReactElementTreeNode => {
let props = {};
if (key) {
props = { key };
}

return {
type: 'ReactElement',
displayName,
props,
defaultProps: {},
childrens,
};
};

export default (
node: ReactPortalTreeNode,
inline: boolean,
lvl: number,
options: Options
): string => {
const { type, containerSelector, childrens } = node;

if (type !== 'ReactPortal') {
throw new Error(
`The "formatReactPortalNode" function could only format node of type "ReactPortal". Given: ${type}`
);
}

return `
{ReactDOM.createPortal(${
childrens.length
? `\n${spacer(lvl + 1, options.tabStop)}${formatReactElementNode(
toReactElementTreeNode('', undefined, childrens),
inline,
lvl + 1,
options
)}\n`
: 'null'
}, document.querySelector(\`${containerSelector}\`))}
`.trim();
};
82 changes: 82 additions & 0 deletions src/formatter/formatReactPortalNode.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* @flow */

import formatReactPortalNode from './formatReactPortalNode';

const defaultOptions = {
filterProps: [],
showDefaultProps: true,
showFunctions: false,
tabStop: 2,
useBooleanShorthandSyntax: true,
useFragmentShortSyntax: true,
sortProps: true,
};

describe('formatReactPortalNode', () => {
it('should format a react portal with a string as children', () => {
const tree = {
type: 'ReactPortal',
containerSelector: 'body',
childrens: [
{
value: 'Hello world',
type: 'string',
},
],
};

expect(formatReactPortalNode(tree, false, 0, defaultOptions))
.toMatchInlineSnapshot(`
"{ReactDOM.createPortal(
<>
Hello world
</>
, document.querySelector(\`body\`))}"
`);
});

it('should format a react portal with multiple childrens', () => {
const tree = {
type: 'ReactPortal',
containerSelector: 'body',
childrens: [
{
type: 'ReactElement',
displayName: 'div',
props: { a: 'foo' },
childrens: [],
},
{
type: 'ReactElement',
displayName: 'div',
props: { b: 'bar' },
childrens: [],
},
],
};

expect(formatReactPortalNode(tree, false, 0, defaultOptions))
.toMatchInlineSnapshot(`
"{ReactDOM.createPortal(
<>
<div a=\\"foo\\" />
<div b=\\"bar\\" />
</>
, document.querySelector(\`body\`))}"
`);
});

it('should format an empty react portal', () => {
const tree = {
type: 'ReactPortal',
containerSelector: 'body',
childrens: [],
};

expect(
formatReactPortalNode(tree, false, 0, defaultOptions)
).toMatchInlineSnapshot(
`"{ReactDOM.createPortal(null, document.querySelector(\`body\`))}"`
);
});
});
5 changes: 5 additions & 0 deletions src/formatter/formatTreeNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import formatReactElementNode from './formatReactElementNode';
import formatReactFragmentNode from './formatReactFragmentNode';
import formatReactPortalNode from './formatReactPortalNode';
import type { Options } from './../options';
import type { TreeNode } from './../tree';

Expand Down Expand Up @@ -54,5 +55,9 @@ export default (
return formatReactFragmentNode(node, inline, lvl, options);
}

if (node.type === 'ReactPortal') {
return formatReactPortalNode(node, inline, lvl, options);
}

throw new TypeError(`Unknow format type "${node.type}"`);
};
18 changes: 18 additions & 0 deletions src/formatter/formatTreeNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ jest.mock('./formatReactElementNode', () => () =>
'<MockedFormatReactElementNodeResult />'
);

jest.mock('./formatReactPortalNode', () => () =>
'<MockedFormatReactPortalNodeResult />'
);

describe('formatTreeNode', () => {
it('should format number tree node', () => {
expect(formatTreeNode({ type: 'number', value: 42 }, true, 0, {})).toBe(
Expand All @@ -19,6 +23,20 @@ describe('formatTreeNode', () => {
);
});

it('should format react portal tree node', () => {
expect(
formatTreeNode(
{
type: 'ReactPortal',
childrens: ['abc'],
},
true,
0,
{}
)
).toBe('<MockedFormatReactPortalNodeResult />');
});

it('should format react element tree node', () => {
expect(
formatTreeNode(
Expand Down
10 changes: 10 additions & 0 deletions src/formatter/getFunctionTypeName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* @flow */

const getFunctionTypeName = (functionType: Function): string => {
if (!functionType.name || functionType.name === '_default') {
return 'Component';
}
return functionType.name;
};

export default getFunctionTypeName;
41 changes: 41 additions & 0 deletions src/formatter/getFunctionTypeName.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @jest-environment jsdom
*/

/* @flow */

import React from 'react';
import getFunctionTypeName from './getFunctionTypeName';

function NamedStatelessComponent(props: { children: React.Children }) {
const { children } = props;
return <div>{children}</div>;
}

const _default = function(props: { children: React.Children }) {
const { children } = props;
return <div>{children}</div>;
};

const NamelessComponent = function(props: { children: React.Children }) {
const { children } = props;
return <div>{children}</div>;
};

delete NamelessComponent.name;

describe('getFunctionTypeName(Component)', () => {
it('getFunctionTypeName(NamedStatelessComponent)', () => {
expect(getFunctionTypeName(NamedStatelessComponent)).toEqual(
'NamedStatelessComponent'
);
});

it('getFunctionTypeName(_default)', () => {
expect(getFunctionTypeName(_default)).toEqual('Component');
});

it('getFunctionTypeName(NamelessComponent)', () => {
expect(getFunctionTypeName(NamelessComponent)).toEqual('Component');
});
});
19 changes: 19 additions & 0 deletions src/formatter/getWrappedComponentDisplayName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* @flow */

import { ForwardRef, Memo } from 'react-is';
import getFunctionTypeName from './getFunctionTypeName';

const getWrappedComponentDisplayName = (Component: *): string => {
switch (true) {
case Boolean(Component.displayName):
return Component.displayName;
case Component.$$typeof === Memo:
return getWrappedComponentDisplayName(Component.type);
case Component.$$typeof === ForwardRef:
return getWrappedComponentDisplayName(Component.render);
default:
return getFunctionTypeName(Component);
}
};

export default getWrappedComponentDisplayName;
Loading