diff --git a/README.md b/README.md
index 2f09ce1e3c..c7c59913a1 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ Enable the rules that you would like to use.
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Detect unescaped HTML entities, which might represent malformed tags
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods
+* [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md): Prevent creating unstable components inside components
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definition of unused state fields
* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of setState in componentWillUpdate
diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md
new file mode 100644
index 0000000000..c19cb2aa13
--- /dev/null
+++ b/docs/rules/no-unstable-nested-components.md
@@ -0,0 +1,134 @@
+# Prevent creating unstable components inside components (react/no-unstable-nested-components)
+
+Creating components inside components without memoization leads to unstable components. The nested component and all its children are recreated during each re-render. Given stateful children of the nested component will lose their state on each re-render.
+
+React reconcilation performs element type comparison with [reference equality](https://github.com/facebook/react/blob/v16.13.1/packages/react-reconciler/src/ReactChildFiber.js#L407). The reference to the same element changes on each re-render when defining components inside the render block. This leads to complete recreation of the current node and all its children. As a result the virtual DOM has to do extra unnecessary work and [possible bugs are introduced](https://codepen.io/ariperkkio/pen/vYLodLB).
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```jsx
+function Component() {
+ function UnstableNestedComponent() {
+ return
+ );
+}
+```
+
+By default component creation is allowed inside component props only if prop name starts with `render`. See `allowAsProps` option for disabling this limitation completely.
+
+```jsx
+function SomeComponent(props) {
+ return
{props.renderFooter()}
;
+}
+
+function Component() {
+ return (
+
+ } />
+
+ );
+}
+```
+
+## Rule Options
+
+```js
+...
+"react/no-unstable-nested-components": [
+ "off" | "warn" | "error",
+ { "allowAsProps": true | false }
+]
+...
+```
+
+You can allow component creation inside component props by setting `allowAsProps` option to true. When using this option make sure you are **calling** the props in the receiving component and not using them as elements.
+
+The following patterns are **not** considered warnings:
+
+```jsx
+function SomeComponent(props) {
+ return
{props.footer()}
;
+}
+
+function Component() {
+ return (
+
+ } />
+
+ );
+}
+```
+
+## When Not To Use It
+
+If you are not interested in preventing bugs related to re-creation of the nested components or do not care about optimization of virtual DOM.
diff --git a/index.js b/index.js
index bc0c30a43d..e40300d7d5 100644
--- a/index.js
+++ b/index.js
@@ -74,6 +74,7 @@ const allRules = {
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
'no-unknown-property': require('./lib/rules/no-unknown-property'),
'no-unsafe': require('./lib/rules/no-unsafe'),
+ 'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js
new file mode 100644
index 0000000000..7cfc2f74fe
--- /dev/null
+++ b/lib/rules/no-unstable-nested-components.js
@@ -0,0 +1,310 @@
+/**
+ * @fileoverview Prevent creating unstable components inside components
+ * @author Ari Perkkiƶ
+ */
+
+'use strict';
+
+const Components = require('../util/Components');
+const docsUrl = require('../util/docsUrl');
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.';
+const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.';
+const RENDER_REGEXP = /^render/;
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Generate error message with given parent component name
+ * @param {String} parentName Name of the parent component
+ * @returns {String} Error message with parent component name
+ */
+function generateErrorMessageWithParentName(parentName) {
+ return `Declare this component outside parent component "${parentName}" or memoize it.`;
+}
+
+/**
+ * Get closest parent matching given matcher
+ * @param {ASTNode} node The AST node
+ * @param {Function} matcher Method used to match the parent
+ * @returns {ASTNode} The matching parent node, if any
+ */
+function getClosestMatchingParent(node, matcher) {
+ if (!node || !node.parent || node.parent.type === 'Program') {
+ return;
+ }
+
+ if (matcher(node.parent)) {
+ return node.parent;
+ }
+
+ return getClosestMatchingParent(node.parent, matcher);
+}
+
+/**
+ * Matcher used to check whether given node is a `createElement` call
+ * @param {ASTNode} node The AST node
+ * @returns {Boolean} True if node is a `createElement` call, false if not
+ */
+function isCreateElementMatcher(node) {
+ return (
+ node
+ && node.type === 'CallExpression'
+ && node.callee
+ && node.callee.property
+ && node.callee.property.name === 'createElement'
+ );
+}
+
+/**
+ * Matcher used to check whether given node is a `ObjectExpression`
+ * @param {ASTNode} node The AST node
+ * @returns {Boolean} True if node is a `ObjectExpression`, false if not
+ */
+function isObjectExpressionMatcher(node) {
+ return node && node.type === 'ObjectExpression';
+}
+
+/**
+ * Resolve the component name of given node
+ * @param {ASTNode} node The AST node of the component
+ * @returns {String} Name of the component, if any
+ */
+function resolveComponentName(node) {
+ const parentName = node.id && node.id.name;
+ if (parentName) return parentName;
+
+ return (
+ node.type === 'ArrowFunctionExpression'
+ && node.parent
+ && node.parent.id
+ && node.parent.id.name
+ );
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Prevent creating unstable components inside components',
+ category: 'Possible Errors',
+ recommended: false,
+ url: docsUrl('no-unstable-nested-components')
+ },
+ schema: [{
+ type: 'object',
+ properties: {
+ customValidators: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ },
+ allowAsProps: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }]
+ },
+
+ create: Components.detect((context, components, utils) => {
+ const allowAsProps = context.options.some((option) => option && option.allowAsProps);
+
+ /**
+ * Check whether given node is declared inside class component's render block
+ * ```jsx
+ * class Component extends React.Component {
+ * render() {
+ * class NestedClassComponent extends React.Component {
+ * ...
+ * ```
+ * @param {ASTNode} node The AST node being checked
+ * @returns {Boolean} True if node is inside class component's render block, false if not
+ */
+ function isInsideRenderMethod(node) {
+ const parentComponent = utils.getParentComponent();
+ if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
+ return false;
+ }
+
+ return (
+ node.parent.type === 'MethodDefinition'
+ && node.parent.key
+ && node.parent.key.name === 'render'
+ );
+ }
+
+ /**
+ * Check whether given node is a function component declared inside class component.
+ * Util's component detection fails to detect function components inside class components.
+ * ```jsx
+ * class Component extends React.Component {
+ * render() {
+ * const NestedComponent = () => ;
+ * ...
+ * ```
+ * @param {ASTNode} node The AST node being checked
+ * @returns {Boolean} True if given node a function component declared inside class component, false if not
+ */
+ function isFunctionComponentInsideClassComponent(node) {
+ const parentComponent = utils.getParentComponent();
+ const parentStatelessComponent = utils.getParentStatelessComponent();
+
+ return (
+ parentComponent
+ && parentStatelessComponent
+ && parentComponent.type === 'ClassDeclaration'
+ && utils.getStatelessComponent(parentStatelessComponent)
+ && utils.isReturningJSX(node)
+ );
+ }
+
+ /**
+ * Check whether given node is declared inside `createElement` call's props
+ * ```js
+ * React.createElement(Component, {
+ * footer: () => React.createElement("div", null)
+ * })
+ * ```
+ * @param {ASTNode} node The AST node
+ * @returns {Boolean} True if node is declare inside `createElement` call's props, false if not
+ */
+ function isComponentInsideCreateElementsProp(node) {
+ if (!components.get(node)) {
+ return false;
+ }
+
+ const createElementParent = getClosestMatchingParent(node, isCreateElementMatcher);
+ if (
+ createElementParent
+ && createElementParent.arguments
+ && createElementParent.arguments[1] === getClosestMatchingParent(node, isObjectExpressionMatcher)) {
+ return true;
+ }
+ }
+
+ /**
+ * Check whether given node is declared inside a component prop.
+ * ```jsx
+ * } />
+ * ```
+ * @param {ASTNode} node The AST node being checked
+ * @returns {Boolean} True if node is a component declared inside prop, false if not
+ */
+ function isComponentInProp(node) {
+ if (node.parent.type !== 'JSXExpressionContainer') {
+ return isComponentInsideCreateElementsProp(node);
+ }
+
+ return utils.isReturningJSX(node);
+ }
+
+ /**
+ * Check whether given node is declared inside a render prop
+ * ```jsx
+ * } />
+ * {() => }
+ * ```
+ * @param {ASTNode} node The AST node
+ * @returns {Boolean} True if component is declared inside a render prop, false if not
+ */
+ function isComponentInRenderProp(node) {
+ if (
+ node.parent.type === 'Property'
+ && node.parent.key
+ && RENDER_REGEXP.test(node.parent.key.name)) {
+ return true;
+ }
+
+ // Check whether component is a render prop used as children, e.g. {() => }
+ const propNode = node.parent.parent;
+ if (propNode && propNode.type === 'JSXElement') {
+ return true;
+ }
+
+ // Check whether prop name starts with render, e.g. } />
+ return (
+ propNode
+ && propNode.type === 'JSXAttribute'
+ && propNode.name
+ && propNode.name.type === 'JSXIdentifier'
+ && RENDER_REGEXP.test(propNode.name.name)
+ );
+ }
+
+ /**
+ * Check whether given node is a unstable nested component
+ * @param {ASTNode} node The AST node being checked
+ */
+ function validate(node) {
+ if (!node || !node.parent) {
+ return;
+ }
+
+ const isDeclaredInsideProps = isComponentInProp(node);
+ if (
+ !components.get(node)
+ && !isFunctionComponentInsideClassComponent(node)
+ && !isDeclaredInsideProps) {
+ return;
+ }
+
+ if (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node))) {
+ return;
+ }
+
+ // Prevent reporting nested class components twice
+ if (isInsideRenderMethod(node)) {
+ return;
+ }
+
+ // Get the closest parent component
+ const parentComponent = getClosestMatchingParent(
+ node,
+ (nodeToMatch) => components.get(nodeToMatch)
+ );
+
+ if (parentComponent) {
+ const parentName = resolveComponentName(parentComponent);
+
+ // Exclude lowercase parents, e.g. function createTestComponent()
+ // React-dom prevents creating lowercase components
+ if (parentName && parentName[0] === parentName[0].toLowerCase()) {
+ return;
+ }
+
+ let message = parentName
+ ? generateErrorMessageWithParentName(parentName)
+ : ERROR_MESSAGE_WITHOUT_NAME;
+
+ // Add information about allowAsProps option when component is declared inside prop
+ if (isDeclaredInsideProps && !allowAsProps) {
+ message += COMPONENT_AS_PROPS_INFO;
+ }
+
+ context.report({node, message});
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------------
+
+ return {
+ FunctionDeclaration(node) { validate(node); },
+ ArrowFunctionExpression(node) { validate(node); },
+ FunctionExpression(node) { validate(node); },
+ ClassDeclaration(node) { validate(node); }
+ };
+ })
+};
diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js
new file mode 100644
index 0000000000..0b3ce2c04f
--- /dev/null
+++ b/tests/lib/rules/no-unstable-nested-components.js
@@ -0,0 +1,974 @@
+/**
+ * @fileoverview Prevent creating unstable components inside components
+ * @author Ari Perkkiƶ
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/no-unstable-nested-components');
+
+const parserOptions = {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true
+ }
+};
+
+const ERROR_MESSAGE = 'Declare this component outside parent component "ParentComponent" or memoize it.';
+const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.';
+const ERROR_MESSAGE_COMPONENT_AS_PROPS = `${ERROR_MESSAGE} If you want to allow component creation in props, set allowAsProps option to true.`;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('no-unstable-nested-components', rule, {
+ valid: [
+ {
+ code: `
+ function OutsideDefinedFunctionComponent() {
+ return ;
+ }
+
+ function ParentComponent() {
+ return (
+