diff --git a/README.md b/README.md
index 68461e0ca9..e9b6bf05da 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,7 @@ Enable the rules that you would like to use.
* [react/no-find-dom-node](docs/rules/no-find-dom-node.md): Prevent usage of findDOMNode
* [react/no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of isMounted
* [react/no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file
+* [react/no-object-type-as-default-prop](docs/rules/no-object-type-as-default-prop.md): Prevent usage of object types variables as default param in functional component
* [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Flag shouldComponentUpdate when extending PureComponent
* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of React.render
* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of setState
diff --git a/docs/rules/no-object-type-as-default-prop.md b/docs/rules/no-object-type-as-default-prop.md
new file mode 100644
index 0000000000..06d4cbc3ed
--- /dev/null
+++ b/docs/rules/no-object-type-as-default-prop.md
@@ -0,0 +1,68 @@
+# Prevent usage of referential-type variables as default param in functional component (react/no-object-type-as-default-prop)
+
+Warns if in a functional component, an object type value (such as array/object literal) is used as default prop, to prevent potential un-necessary re-renders, and performance regressions.
+
+## Rule Details
+
+Certain values (like arrays, objects, functions, etc) are compared by indentity instead of by value. This means that, for example, whilst two empty arrays represent the same value - to the JS engine they are distinct and inequal as they represent two different identities in memory.
+
+When using object destructuring syntax you can set the default value for a given property if it does not exist. If you set the default value to one of the values that is compared by identity, it will mean that each time the destructure is executed the JS engine will create a new, non-equal value in the destructured variable.
+
+In the context of a React functional component's props argument this means that each render the property has a new, distinct value. When this value is passed to a hook as a dependency or passed into a child component as a property react will see this as a new value - meaning that a hook will be re-evaluated, or a memoized component will rerender.
+
+This obviously destroys any performance benefits you get from memoisation. Additionally in certain circumstances this can cause infinite rerender loops, which can often be hard to debug.
+
+It's worth noting that primitive literal values (`string`, `number`, `boolean`, `null`, and `undefined`) are compared by value - meaning these are safe to use as inlined default destructured property values.
+
+To fix the violations, the easiest way is to use a referencing variable instead of using the literal values, e.g:
+
+```
+const emptyArray = [];
+
+function Component({
+ items = emptyArray,
+}) {}
+```
+
+Examples of ***invalid*** code for this rule:
+
+```jsx
+function Component({
+ items = [],
+}) {}
+
+const Component = ({
+ items = {},
+}) => {}
+
+const Component = ({
+ items = () => {},
+}) => {}
+```
+
+Examples of ***valid*** code for this rule:
+
+```jsx
+const emptyArray = [];
+
+function Component({
+ items = emptyArray,
+}) {}
+
+const emptyObject = {};
+const Component = ({
+ items = emptyObject,
+}) => {}
+
+const noopFunc = () => {};
+const Component = ({
+ items = noopFunc,
+}) => {}
+
+// primitive literals are all compared by value, so are safe to be inlined
+function Component({
+ num = 3,
+ str = 'foo',
+ bool = true,
+}) {}
+```
diff --git a/index.js b/index.js
index 8edb1177f1..de0fb296ba 100644
--- a/index.js
+++ b/index.js
@@ -67,6 +67,7 @@ const allRules = {
'no-find-dom-node': require('./lib/rules/no-find-dom-node'),
'no-is-mounted': require('./lib/rules/no-is-mounted'),
'no-multi-comp': require('./lib/rules/no-multi-comp'),
+ 'no-object-type-as-default-prop': require('./lib/rules/no-object-type-as-default-prop'),
'no-set-state': require('./lib/rules/no-set-state'),
'no-string-refs': require('./lib/rules/no-string-refs'),
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
diff --git a/lib/rules/no-object-type-as-default-prop.js b/lib/rules/no-object-type-as-default-prop.js
new file mode 100644
index 0000000000..553d30823f
--- /dev/null
+++ b/lib/rules/no-object-type-as-default-prop.js
@@ -0,0 +1,152 @@
+/**
+ * @fileoverview Prevent usage of referential-type variables as default param in functional component
+ * @author Chang Yan
+ */
+
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+
+const FORBIDDEN_TYPES_MAP = {
+ ArrowFunctionExpression: 'arrow function',
+ FunctionExpression: 'function expression',
+ ObjectExpression: 'object literal',
+ ArrayExpression: 'array literal',
+ ClassExpression: 'class expression',
+ NewExpression: 'construction expression',
+ JSXElement: 'JSX element'
+};
+
+const FORBIDDEN_TYPES = new Set(Object.keys(FORBIDDEN_TYPES_MAP));
+const MESSAGE_ID = 'forbiddenTypeDefaultParam';
+
+function isReactComponentName(node) {
+ if (node.id && node.id.type === 'Identifier' && node.id.name) {
+ const firstLetter = node.id.name[0];
+ if (firstLetter.toUpperCase() === firstLetter) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function isReactComponentVariableDeclarator(variableDeclarator) {
+ if (!isReactComponentName(variableDeclarator)) {
+ return false;
+ }
+ return (
+ variableDeclarator.init != null
+ && variableDeclarator.init.type === 'ArrowFunctionExpression'
+ );
+}
+
+function hasUsedObjectDestructuringSyntax(params) {
+ if (
+ params == null
+ || params.length !== 1
+ || params[0].type !== 'ObjectPattern'
+ ) {
+ return false;
+ }
+ return true;
+}
+
+function verifyDefaultPropsDestructuring(context, properties) {
+ // Loop through each of the default params
+ properties.forEach((prop) => {
+ if (prop.type !== 'Property') {
+ return;
+ }
+
+ const propName = prop.key.name;
+ const propDefaultValue = prop.value;
+
+ if (propDefaultValue.type !== 'AssignmentPattern') {
+ return;
+ }
+
+ const propDefaultValueType = propDefaultValue.right.type;
+
+ if (
+ propDefaultValueType === 'Literal'
+ && propDefaultValue.right.regex != null
+ ) {
+ context.report({
+ node: propDefaultValue,
+ messageId: MESSAGE_ID,
+ data: {
+ propName,
+ forbiddenType: 'regex literal'
+ }
+ });
+ } else if (propDefaultValueType === 'CallExpression'
+ && propDefaultValue.right.callee.type === 'MemberExpression'
+ && propDefaultValue.right.callee.object.type === 'Identifier'
+ && propDefaultValue.right.callee.object.name === 'Symbol'
+ && propDefaultValue.right.callee.property.name === 'for'
+ ) {
+ context.report({
+ node: propDefaultValue,
+ messageId: MESSAGE_ID,
+ data: {
+ propName,
+ forbiddenType: 'Symbol.for literal'
+ }
+ });
+ } else if (FORBIDDEN_TYPES.has(propDefaultValueType)) {
+ context.report({
+ node: propDefaultValue,
+ messageId: MESSAGE_ID,
+ data: {
+ propName,
+ forbiddenType: FORBIDDEN_TYPES_MAP[propDefaultValueType]
+ }
+ });
+ }
+ });
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Prevent usage of referential-type variables as default param in functional component',
+ category: 'Best Practices',
+ recommended: false,
+ url: docsUrl('no-object-type-as-default-prop')
+ },
+ messages: {
+ [MESSAGE_ID]:
+ '{{propName}} has a/an {{forbiddenType}} as default prop.\n'
+ + 'This could lead to potential infinite render loop in React. \n'
+ + 'Use a variable reference instead of {{forbiddenType}}.'
+ }
+ },
+ create(context) {
+ return {
+ FunctionDeclaration(node) {
+ if (
+ !isReactComponentName(node)
+ || !hasUsedObjectDestructuringSyntax(node.params)
+ ) {
+ return;
+ }
+
+ const properties = node.params[0].properties;
+ verifyDefaultPropsDestructuring(context, properties);
+ },
+ 'VariableDeclarator > :matches(ArrowFunctionExpression, FunctionExpression).init'(
+ node
+ ) {
+ if (
+ !isReactComponentVariableDeclarator(node.parent)
+ || !hasUsedObjectDestructuringSyntax(node.params)
+ ) {
+ return;
+ }
+ const properties = node.params[0].properties;
+ verifyDefaultPropsDestructuring(context, properties);
+ }
+ };
+ }
+};
diff --git a/tests/lib/.DS_Store b/tests/lib/.DS_Store
new file mode 100644
index 0000000000..23bac10b4c
Binary files /dev/null and b/tests/lib/.DS_Store differ
diff --git a/tests/lib/rules/no-object-type-as-default-prop.js b/tests/lib/rules/no-object-type-as-default-prop.js
new file mode 100644
index 0000000000..0df64e2582
--- /dev/null
+++ b/tests/lib/rules/no-object-type-as-default-prop.js
@@ -0,0 +1,241 @@
+/**
+ * @fileoverview Prevent usage of object type variables as default param in functional component
+ * @author Chang Yan
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester;
+const parsers = require('../../helpers/parsers');
+const rule = require('../../../lib/rules/no-object-type-as-default-prop');
+
+const parserOptions = {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true
+ }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+const MESSAGE_ID = 'forbiddenTypeDefaultParam';
+
+const expectedViolations = [
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'a',
+ forbiddenType: 'object literal'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'b',
+ forbiddenType: 'array literal'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'c',
+ forbiddenType: 'regex literal'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'd',
+ forbiddenType: 'arrow function'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'e',
+ forbiddenType: 'function expression'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'f',
+ forbiddenType: 'class expression'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'g',
+ forbiddenType: 'construction expression'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'h',
+ forbiddenType: 'JSX element'
+ }
+ },
+ {
+ messageId: MESSAGE_ID,
+ data: {
+ propName: 'i',
+ forbiddenType: 'Symbol.for literal'
+ }
+ }
+];
+
+ruleTester.run('no-object-type-as-default-prop', rule, {
+ valid: [
+ `
+ function Foo({
+ bar = emptyFunction,
+ }) {}
+ `,
+ `
+ function Foo({
+ bar = emptyFunction,
+ ...rest
+ }) {}
+ `,
+ `
+ function Foo({
+ bar = 1,
+ baz = 'hello',
+ }) {}
+ `,
+ `
+ function Foo(props) {}
+ `,
+ `
+ function Foo(props) {}
+
+ Foo.defaultProps = {
+ bar: () => {}
+ }
+ `,
+ `
+ const Foo = () => {};
+ `,
+ `
+ const Foo = ({bar = 1}) => {};
+ `,
+ `
+ // It's hard to tell if an anonymous function is a
+ // React component or not, so we simply skip it
+ // to prevent false positive
+ export default function({foo = {}}) {}
+ `
+ ],
+ invalid: [
+ {
+ code: `
+ function Foo({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) {}
+ `,
+ errors: expectedViolations
+ },
+ {
+ code: `
+ function Foo({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) {}
+ `,
+ parser: parsers.BABEL_ESLINT,
+ errors: expectedViolations
+ },
+ {
+ code: `
+ function Foo({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) {}
+ `,
+ parser: parsers.TYPESCRIPT_ESLINT,
+ errors: expectedViolations
+ },
+ {
+ code: `
+ const Foo = ({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) => {}
+ `,
+ errors: expectedViolations
+ },
+ {
+ code: `
+ const Foo = ({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) => {}
+ `,
+ errors: expectedViolations,
+ parser: parsers.BABEL_ESLINT
+ },
+ {
+ code: `
+ const Foo = ({
+ a = {},
+ b = ['one', 'two'],
+ c = /regex/i,
+ d = () => {},
+ e = function() {},
+ f = class {},
+ g = new Thing(),
+ h = ,
+ i = Symbol.for('foo')
+ }) => {}
+ `,
+ errors: expectedViolations,
+ parser: parsers.TYPESCRIPT_ESLINT
+ }
+ ]
+});