Skip to main content
interlace
Plugin: react-featuresRules

no-unnecessary-rerenders

Report an object, array or function created during render and passed to a component that compares props by identity.

Quick Summary

AspectDetails
SeveritySuggestion (performance)
Auto-FixNo (suggestions: useMemo / useCallback)
CategoryPerformance
Best ForComponents 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(...), a Component class): 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

OptionTypeDefaultDescription
minSizenumber5Smallest object / array literal reported. Functions are reported at any size.
ignoreInTestsbooleantrueSkip *.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 minSize passed 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.

Further Reading

Use this rule

npm install -D oxlint-plugin-react-features
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-react-features"],  "rules": {    "react-features/no-unnecessary-rerenders": "error"  }}
vite.config.ts
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.