no-unnecessary-rerenders
Report an object, array or function created during render and passed to a component that compares props by identity.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Suggestion (performance) |
| Auto-Fix | No (suggestions: useMemo / useCallback) |
| Category | Performance |
| Best For | Components that use memo or PureComponent |
Rule Details
An object literal, an array literal or an inline function written as a prop is created anew on every render, so it never equals the value from the last render. That matters only when the receiver skips re-rendering on equal props:
- a component the file wraps in React's
memo(memo(...),React.memo(...), a renamed import,memo(forwardRef(...))), or - a class extending React's
PureComponent.
There, a fresh value defeats the memoization and the child re-renders every time its parent does. The rule reports it.
It does not report:
- a host element (
<div style={{…}}>,<button onClick={() => …}>,<my-widget>): React sets the new value on the DOM node; there is no render to save; - a component the file defines without
memo(a function, an arrow,forwardRef(...), aComponentclass): it re-renders with its parent whatever its props are; - a value that is not created in render (an identifier, a call result, a string or number).
A conditional whose two branches are both fresh values of the same kind
(flag ? () => a() : () => b()) is a fresh value; TypeScript's as,
satisfies and ! are seen through.
Options
| Option | Type | Default | Description |
|---|---|---|---|
minSize | number | 5 | Smallest object / array literal reported. Functions are reported at any size. |
ignoreInTests | boolean | true | Skip *.test.* / *.spec.* files. |
Examples
❌ Incorrect
import { memo } from 'react';
const Row = memo(function Row({ onSelect }: Props) {
return <li onClick={onSelect}>…</li>;
});
function List({ items }: { items: Item[] }) {
// A new function every render: Row's memo never holds.
return items.map((item) => <Row key={item.id} onSelect={() => select(item)} />);
}✅ Correct
function List({ items }: { items: Item[] }) {
const onSelect = useCallback((item: Item) => select(item), []);
return items.map((item) => <Row key={item.id} item={item} onSelect={onSelect} />);
}
// A host element has no render to save.
<button onClick={() => save()}>Save</button>;Known False Positives
- A component the file cannot see (an import,
<Ctx.Provider>,<Ui.Row>) is still checked: whether it compares props by identity is unknown, so the finding may not hold.
Known False Negatives
- An object or array smaller than
minSizepassed to a memo component also defeats the memo; it is not reported below the threshold. - A fresh value produced by a call (
debounce(() => …),{...a}spread into a new object through a helper) is not followed.
Related Rules
Further Reading
- memo - when props are compared
- useMemo and useCallback
Use this rule
npm install -D oxlint-plugin-react-features{ "jsPlugins": ["oxlint-plugin-react-features"], "rules": { "react-features/no-unnecessary-rerenders": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-react-features'], rules: { 'react-features/no-unnecessary-rerenders': 'error', }, },});oxlint-plugin-react-features re-exports eslint-plugin-react-features/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.