Skip to content

Commit

Permalink
fix(is/plainObject): extend isPlainObject check for React components (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
meskill committed Sep 2, 2019
1 parent 56863f8 commit c78ac1c
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 19 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@
"jsdom": "^11.12.0",
"ora": "^2.1.0",
"prettier": "^1.16.4",
"react": "^16.4.1",
"react": "^16.9.0",
"react-is": "^16.9.0",
"recursive-readdir-sync": "^1.0.6",
"ts-jest": "^23.1.3",
"typescript": "^3.0.1",
"walker": "^1.0.7"
},
"dependencies": {}
"peerDependencies": {
"react-is": "*"
}
}
15 changes: 15 additions & 0 deletions src/__tests__/clone.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import clone from '../clone';
const { createElement, memo } = require('react');

describe('utils/clone', () => {
it('should return copy of all nested objects', () => {
Expand Down Expand Up @@ -32,4 +33,18 @@ describe('utils/clone', () => {
expect(result.regex2).not.toBe(regex2);
expect(result.regex2).toEqual(regex2);
});

it('should not copy react elements', () => {
const elem = createElement('i');
const memElem = memo(() => createElement('i'));

const obj = { elem, memElem };
const result = clone(obj);

expect(result).toEqual(obj);
expect(result).not.toBe(obj);

expect(result.elem).toBe(elem);
expect(result.memElem).toBe(memElem);
});
});
10 changes: 7 additions & 3 deletions src/clone.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type from './type';
import mapObj from './object/map';
import mapArr from './array/map';
import isPlainObject from './is/plainObject';

interface Clone {
<T>(x: T): T;
Expand Down Expand Up @@ -32,16 +33,19 @@ const cloneRegExp = (pattern: RegExp) =>
const clone = (x) => {
switch (type(x)) {
case 'Object':
return mapObj(clone, x);
if (isPlainObject(x)) {
return mapObj(clone, x);
}
break;
case 'Array':
return mapArr(clone, x);
case 'Date':
return new Date(x.valueOf());
case 'RegExp':
return cloneRegExp(x);
default:
return x;
}

return x;
};

export default clone as Clone;
3 changes: 2 additions & 1 deletion src/is/__tests__/plainObject.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import isPlainObject from '../plainObject';
const { createElement } = require('react');
const { createElement, memo } = require('react');

describe('utils/is/plainObject', () => {
it('test', () => {
expect(isPlainObject({ a: 5 })).toBe(true);
expect(isPlainObject({})).toBe(true);
expect(isPlainObject(new Date())).toBe(false);
expect(isPlainObject(createElement('span'))).toBe(false);
expect(isPlainObject(memo('span'))).toBe(false);
expect(isPlainObject(5)).toBe(false);
expect(isPlainObject('')).toBe(false);
expect(isPlainObject(null)).toBe(false);
Expand Down
41 changes: 41 additions & 0 deletions src/is/__tests__/reactComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { createElement, PureComponent, memo, lazy } = require('react');

let mockReactIs;

jest.mock('react-is', () => mockReactIs);

class Component extends PureComponent {}

describe('utils/is/reactComponent', () => {
beforeEach(() => {
jest.resetModules();
});

it('test', () => {
mockReactIs = require.requireActual('react-is');
const isReactComponent = require('../reactComponent');

expect(isReactComponent({ test: 'i' })).toBe(false);
expect(isReactComponent(createElement('i'))).toBe(false);
expect(isReactComponent({ $$typeof: Symbol.for('react.element') })).toBe(false);
expect(isReactComponent('test')).toBe(true);
expect(isReactComponent(() => createElement('i'))).toBe(true);
expect(isReactComponent(Component)).toBe(true);
expect(isReactComponent(memo(Component))).toBe(true);
expect(isReactComponent(lazy(Component))).toBe(true);
});

it('test when react-is not defined', () => {
mockReactIs = null;
const isReactComponent = require('../reactComponent');

expect(isReactComponent({ test: 'i' })).toBe(false);
expect(isReactComponent(createElement('i'))).toBe(true);
expect(isReactComponent({ $$typeof: Symbol.for('react.element') })).toBe(true);
expect(isReactComponent('test')).toBe(true);
expect(isReactComponent(() => createElement('i'))).toBe(true);
expect(isReactComponent(Component)).toBe(true);
expect(isReactComponent(memo(Component))).toBe(true);
expect(isReactComponent(lazy(Component))).toBe(true);
});
});
23 changes: 22 additions & 1 deletion src/is/__tests__/reactElement.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import isReactElement from '../reactElement';
const { createElement, PureComponent } = require('react');

let mockReactIs;

jest.mock('react-is', () => mockReactIs);

class Component extends PureComponent {}

describe('utils/is/reactElement', () => {
beforeEach(() => {
jest.resetModules();
});

it('test', () => {
mockReactIs = require.requireActual('react-is');
const isReactElement = require('../reactElement');

expect(isReactElement('test')).toBe(false);
expect(isReactElement({})).toBe(false);
expect(isReactElement(new Component())).toBe(false);
expect(isReactElement(createElement('i'))).toBe(true);
expect(isReactElement({ $$typeof: Symbol.for('react.element') })).toBe(true);
});

it('test when is-react not defined', () => {
mockReactIs = null;
const isReactElement = require('../reactElement');

expect(isReactElement('test')).toBe(false);
expect(isReactElement({})).toBe(false);
expect(isReactElement(new Component())).toBe(false);
Expand Down
5 changes: 3 additions & 2 deletions src/is/plainObject.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import isReactElement from './reactElement';
import isReactComponent from './reactComponent';
import isObject from './object';

/**
* Returns whether a value is a plain object
* (an object that is created using an object literal, Object.create(null) or similar).
* Rejects anything with a custom prototype or a non-object ECMAScript type.
* Also rejects React elements
* Also rejects React elements and components
*
* **Note:** if the host environment does not support Symbol, any object with a $$typeof
* property is considered a React element
Expand Down Expand Up @@ -38,7 +39,7 @@ export default function isPlainObject(test): test is Record<any, any> {
return false;
}

if (isReactElement(test)) {
if (isReactElement(test) || isReactComponent(test)) {
// react elements _are_ plain objects, but we don't treat them like that
// (e.g. in recursive merges)
return false;
Expand Down
24 changes: 24 additions & 0 deletions src/is/reactComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import has from '../object/has';

let isComponent: (test: any) => boolean;

try {
isComponent = require('react-is').isValidElementType;
} catch (e) {}

if (!isComponent) {
isComponent = (test) => typeof test === 'string' || typeof test === 'function' || (!!test && has('$$typeof', test));
}

/**
* Returns whether a value is a valid React component
*
* **Note:** uses `react-is` library internally. If the host environment does not has `react-is` library,
* any strings, function or object with $$typeof property are considered valid.
*
* **Note:**
*
* @param {*} test a reference being tested
* @returns whether a value is a React component
*/
export default isComponent;
23 changes: 13 additions & 10 deletions src/is/reactElement.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import has from '../object/has';

const HAS_SYMBOL_SUPPORT = typeof Symbol === 'function' && typeof Symbol.for === 'function';
const REACT_ELEMENT_TYPE = HAS_SYMBOL_SUPPORT && Symbol.for('react.element');
let isElement: (test: any) => boolean;

try {
isElement = require('react-is').isElement;
} catch (e) {}

if (!isElement) {
isElement = (test) => !!test && has('$$typeof', test);
}

/**
* Returns whether a value is a valid React element
*
* **Note:** this won't work with any API-compatible libraries like Inferno
* or Preact which check virtual DOM node integrity through other means
* (binary flags and instanceof checks respectively)
* **Note:** uses `react-is` library internally. If the host environment does not has `react-is` library,
* any object with $$typeof property is considered valid.
*
* **Note:** if the host environment does not support Symbol, any $$typeof
* property value is considered valid.
* **Note:**
*
* @param {*} test a reference being tested
* @returns whether a value is a React element
*/
export default function isReactElement(test): boolean {
return !!test && has('$$typeof', test) && (!HAS_SYMBOL_SUPPORT || test.$$typeof === REACT_ELEMENT_TYPE);
}
export default isElement;

0 comments on commit c78ac1c

Please sign in to comment.