A component defined inside another component is a new function every time the parent renders. React compares components by identity, so it sees a different component each time. It removes the old DOM nodes, builds new ones, and throws away the state of everything below. Inputs lose focus and their text, and effects run again.
Move the inner component to the top level of the file and pass it what it needs as props.
Configure
Off by default. To turn it on:
[lint.rules]
"react/no-unstable-nested-components" = "error" # or "warn"
Every option, set to its default:
[lint.rules]
"react/no-unstable-nested-components" = ["error", {
allowAsProps = false,
propNamePattern = "render*",
customValidators = [], # list of strings
}]
Examples
Reported:
function UserList({ users }) {
function Row({ user }) {
return <li>{user.name}</li>;
}
return (
<ul>
{users.map(user => (
<Row key={user.id} user={user} />
))}
</ul>
);
}
A component created in a prop is reported too:
function Page() {
return <Layout footer={() => <small>Terms of use</small>} />;
}
Not reported:
function Row({ user }) {
return <li>{user.name}</li>;
}
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<Row key={user.id} user={user} />
))}
</ul>
);
}
A function passed as children, or in a prop whose name starts with render, counts as a render prop. The receiver calls it as a plain function, so nothing is remounted:
function Page() {
return <Layout renderFooter={() => <small>Terms of use</small>} />;
}
Options
| Option | Default | What it does |
|---|---|---|
allowAsProps | false | Allow a component that's created in a prop. |
propNamePattern | "render*" | A glob that matches the names of render props. |
customValidators | [] | Accepted, but has no effect. |
function Page() {
return <Layout footer={() => <small>Terms of use</small>} />;
}
function Report({ rows }) {
return <Table rows={rows} rowRenderer={row => <Row data={row} />} />;
}