Category: Performance - Broken Memoization Severity: Warning Cross-file Analysis: ✅ Required Auto-fixable: No (requires architectural decisions)
Detects when unstable props (inline objects, arrays, or functions) are passed to memoized components, completely breaking memoization and causing unnecessary re-renders. This rule requires cross-file analysis to detect violations that ESLint cannot catch.
React's memoization features (React.memo, useMemo, useCallback) prevent unnecessary re-renders by comparing props/dependencies by reference. When you pass inline objects, arrays, or functions, you create a new reference every render, breaking memoization entirely.
Broken memoization means:
- ❌ Memoized component re-renders on every parent render
- ❌ Performance is worse than without memoization (overhead without benefit)
- ❌ The entire component subtree re-renders unnecessarily
- ❌ Wasted CPU cycles, memory allocations, and DOM updates
This rule currently detects:
Detects when a parent component passes unstable props to a React.memo component defined in another file.
❌ Violation:
// MemoChild.tsx
export const MemoChild = React.memo(({ config, items, onUpdate }) => {
return (
<div>
{config.theme} - {items.length} items
<button onClick={onUpdate}>Update</button>
</div>
);
});
// Parent.tsx
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
{/* ❌ VIOLATION: Inline object - new reference every render */}
<MemoChild config={{ theme: 'dark' }} />
{/* ❌ VIOLATION: Inline array - new reference every render */}
<MemoChild items={[1, 2, 3]} />
{/* ❌ VIOLATION: Inline function - new reference every render */}
<MemoChild onUpdate={() => console.log('update')} />
{/* All three violations cause MemoChild to re-render on EVERY Parent render */}
</div>
);
}Why ESLint Can't Detect This:
- Requires analyzing
MemoChild.tsxto know it's memoized - Requires analyzing
Parent.tsxto find unstable props - Must follow imports across files
- Needs semantic understanding of
React.memowrapping
✅ Correct Patterns:
// Option 1: Extract to module-level constants
const CONFIG = { theme: 'dark' };
const ITEMS = [1, 2, 3];
function Parent() {
const handleUpdate = useCallback(() => {
console.log('update');
}, []);
return (
<div>
<MemoChild config={CONFIG} /> {/* ✅ Same reference every render */}
<MemoChild items={ITEMS} /> {/* ✅ Same reference every render */}
<MemoChild onUpdate={handleUpdate} /> {/* ✅ Stable with useCallback */}
</div>
);
}// Option 2: Use useMemo for objects/arrays
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
const items = useMemo(() => [1, 2, 3], []);
return <MemoChild config={config} items={items} />;
}// Option 3: If props change based on state, include in dependencies
function Parent() {
const [theme, setTheme] = useState('dark');
const config = useMemo(() => ({ theme }), [theme]);
return <MemoChild config={config} />;
}Status: Partially implemented, currently disabled
This detection will catch when components use useMemo or useCallback with prop dependencies that are unstable objects from parent components.
// TODO: Not yet implemented
function Child({ config }) {
// If parent passes inline object to config,
// this useMemo will re-run every render
const computed = useMemo(() => {
return expensiveCalculation(config);
}, [config]); // Will be detected once implemented
return <div>{computed}</div>;
}- Find JSX elements in current file (e.g.,
<MemoChild />) - Extract component name from the element
- Check if imported - look through import statements
- Resolve import path - follow the import to source file
- Load target module - parse and analyze the source file
- Check if memoized - detect
React.memo()wrapping - Analyze props - check for inline objects/arrays/functions
- Report violations with precise line numbers
Unstable (detected):
- Inline object literals:
{ theme: 'dark' } - Inline array literals:
[1, 2, 3] - Inline arrow functions:
() => console.log() - Inline function expressions:
function() { ... }
Stable (allowed):
- Variables defined outside component:
const CONFIG = { ... } - Props wrapped in
useMemo:useMemo(() => ({ ... }), [deps]) - Props wrapped in
useCallback:useCallback(() => { ... }, [deps]) - Primitive values: strings, numbers, booleans, null, undefined
- References to stable variables
React.memo is beneficial when:
✅ Use React.memo when:
- Component is expensive to render
- Component receives the same props frequently
- Component is in a list that re-renders often
- You can provide stable props (or use useMemo/useCallback)
❌ Don't use React.memo when:
- Props are always unstable (inline objects/arrays/functions)
- Component is cheap to render
- Props change frequently
- You're not prepared to stabilize props with useMemo/useCallback
// ❌ Broken memoization is WORSE than no memoization
const MemoChild = React.memo(Child);
function Parent() {
const [count, setCount] = useState(0);
return (
<>
{/* Every count change causes:
1. Parent renders
2. New config object created
3. React.memo compares props (overhead)
4. Props are different (new reference)
5. MemoChild renders anyway
Result: Memoization overhead + full render = slower than no memo */}
<MemoChild config={{ theme: 'dark' }} />
</>
);
}// ✅ Working memoization skips child renders
const MemoChild = React.memo(Child);
const CONFIG = { theme: 'dark' };
function Parent() {
const [count, setCount] = useState(0);
return (
<>
{/* Every count change:
1. Parent renders
2. CONFIG reference unchanged
3. React.memo compares props (overhead)
4. Props are same (same reference)
5. MemoChild render skipped ✅
Result: Only memoization overhead, no child render */}
<MemoChild config={CONFIG} />
</>
);
}❌ Problem:
<Component config={{ apiUrl: '/api', timeout: 5000 }} />✅ Solutions:
// Option A: Module constant
const API_CONFIG = { apiUrl: '/api', timeout: 5000 };
<Component config={API_CONFIG} />
// Option B: useMemo
const config = useMemo(() => ({ apiUrl: '/api', timeout: 5000 }), []);
<Component config={config} />❌ Problem:
<List items={items.filter(x => x.active)} />✅ Solution:
const activeItems = useMemo(
() => items.filter(x => x.active),
[items]
);
<List items={activeItems} />❌ Problem:
<Button onClick={() => handleClick(id)} />✅ Solutions:
// Option A: useCallback with dependency
const handleButtonClick = useCallback(() => {
handleClick(id);
}, [id]);
<Button onClick={handleButtonClick} />
// Option B: Pass data through props
<Button onClick={handleClick} data={id} />
// Then in Button: onClick={() => props.onClick(props.data)}❌ Problem:
<Component style={{ padding: 20, margin: 10 }} />✅ Solutions:
// Option A: Module constant
const STYLE = { padding: 20, margin: 10 };
<Component style={STYLE} />
// Option B: CSS classes (preferred)
<Component className="padded-box" />- useMemo/useCallback with unstable prop dependencies - Partially implemented, disabled
- Unstable props from state/context - Only detects inline literals currently
- Props constructed from functions -
items={getData()}not tracked yet - Conditional unstable props -
config={show ? {...} : null}may not be caught
- Same-file memoization - Detected (implemented in Issue 3)
- Inline primitives - Not flagged (primitives are compared by value)
- Spread operators -
{...config}not detected as spreading may be intentional
- no-object-deps - Detects unstable objects in hook dependency arrays
- no-inline-props - Would detect ALL inline props (not just to memo components)
❌ Before (broken memoization):
// Table.tsx
export const Table = React.memo(({ columns, data, onSort }) => {
// Expensive rendering logic
return <table>...</table>;
});
// Dashboard.tsx
function Dashboard() {
const [sortKey, setSortKey] = useState('name');
return (
<Table
columns={[
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' }
]}
data={userData}
onSort={(key) => setSortKey(key)}
/>
);
// Table re-renders on EVERY Dashboard render
// Memoization is completely broken
}✅ After (working memoization):
// Table.tsx - unchanged
export const Table = React.memo(({ columns, data, onSort }) => {
return <table>...</table>;
});
// Dashboard.tsx
const COLUMNS = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' }
];
function Dashboard() {
const [sortKey, setSortKey] = useState('name');
const handleSort = useCallback((key) => {
setSortKey(key);
}, []);
return (
<Table
columns={COLUMNS}
data={userData}
onSort={handleSort}
/>
);
// Table only re-renders when userData changes
// Memoization working perfectly
}Performance Improvement:
- Before: Table re-renders ~10-20 times per second during interactions
- After: Table re-renders only when data actually changes
- Result: Smooth 60fps instead of janky interactions
This rule has no configuration options. It always checks for unstable props to memoized components.
You might want to disable this rule if:
- You're not using
React.memoin your codebase - You prefer a different memoization strategy (e.g., Recoil selectors)
- You're in a code section where performance is not a concern
- React Docs: React.memo
- React Docs: useMemo
- React Docs: useCallback
- Before You memo()
- Why React Re-Renders
This rule uses the ModuleResolver to:
- Parse and cache imported modules
- Follow import statements across files
- Detect
React.memowrapping in target modules - Track aliased imports correctly (e.g.,
import { MemoChild as FastChild })
jsx_elementandjsx_self_closing_elementfor component usagecall_expressionwithReact.memoormemofunctionobjectandarraynode types for inline literalsarrow_functionandfunctionfor inline functionsmember_expressionfor import resolution with aliases