This ESLint warning occurs when your useMemo hook references variables, props, or state that are not included in its dependency array. When dependencies are missing, your memoized value won't recalculate when those values change, leading to stale calculations and performance issues.
The "React Hook useMemo has a missing dependency" warning is triggered by ESLint's react-hooks/exhaustive-deps rule. This rule analyzes your useMemo hooks and compares the variables, props, state, and functions referenced inside the memoization callback against the dependency array you provided. useMemo is designed to cache expensive calculations and prevent unnecessary re-computation. When you use a value inside the useMemo callback that can change over time (like props, state, or functions defined in the component), React needs to know about it through the dependency array. This tells React when to re-run the calculation. If you omit a dependency, the memoized value will continue using the old value from when it first ran, even after the dependency has changed. This leads to stale values being used in your component, which can cause incorrect UI rendering, outdated calculations, and subtle bugs that are hard to debug. The ESLint rule exists to catch these issues before they become runtime problems.
Review your useMemo callback and list every variable that comes from outside the memoization scope. This includes props, state, context values, and functions.
function ProductList({ products, sortOrder }) {
const [filter, setFilter] = useState('');
const filteredProducts = useMemo(() => {
// products, sortOrder, and filter are dependencies
return products
.filter(p => p.name.includes(filter))
.sort((a, b) => a[sortOrder] - b[sortOrder]);
}, [products]); // ❌ Missing sortOrder and filter!
}Look for variables that can change between renders. Constants defined outside the component are safe to omit.
Include every dependency in the array. This ensures the memoized value recalculates when any dependency changes.
function ProductList({ products, sortOrder }) {
const [filter, setFilter] = useState('');
const filteredProducts = useMemo(() => {
return products
.filter(p => p.name.includes(filter))
.sort((a, b) => a[sortOrder] - b[sortOrder]);
}, [products, sortOrder, filter]); // ✅ All dependencies included
}The memoized value will now recalculate whenever products, sortOrder, or filter changes.
If you have calculations that don't depend on reactive values, move them outside useMemo to reduce dependencies.
// Before: mixed stable and reactive calculations
function ProductList({ products, taxRate }) {
const processedProducts = useMemo(() => {
const basePrice = 100; // This is constant
return products.map(p => ({
...p,
price: basePrice * (1 + taxRate)
}));
}, [products, taxRate]); // taxRate is needed, basePrice is not
// After: separate stable calculations
function ProductList({ products, taxRate }) {
const basePrice = 100; // Constant outside useMemo
const processedProducts = useMemo(() => {
return products.map(p => ({
...p,
price: basePrice * (1 + taxRate)
}));
}, [products, taxRate]); // basePrice not needed
}
}If your useMemo depends on a function, wrap it with useCallback to prevent unnecessary re-calculations.
function ProductList({ products, userId }) {
// Without useCallback: function recreated every render
const calculateDiscount = useCallback((price) => {
return price * (0.9 + (userId % 10) / 100);
}, [userId]); // calculateDiscount only changes when userId changes
const discountedProducts = useMemo(() => {
return products.map(p => ({
...p,
discountedPrice: calculateDiscount(p.price)
}));
}, [products, calculateDiscount]); // ✅ Safe dependency
}This ensures the function reference stays stable unless its dependencies change.
When useMemo depends on objects or arrays, use another useMemo to stabilize them and prevent infinite re-calculations.
function ProductList({ products, options }) {
// Without stabilization: new options object every render
const processedProducts = useMemo(() => {
return products.map(p => processProduct(p, options));
}, [products, options]); // ❌ options changes every render
// With stabilization: options memoized
const stableOptions = useMemo(() => options, [
options.sortBy,
options.filterBy,
options.pageSize
]); // Only depend on primitive values
const processedProducts = useMemo(() => {
return products.map(p => processProduct(p, stableOptions));
}, [products, stableOptions]); // ✅ Stable dependency
}useMemo adds overhead. If the calculation is cheap or dependencies change frequently, consider removing useMemo entirely.
// Before: unnecessary useMemo
function ProductList({ products }) {
const productCount = useMemo(() => {
return products.length;
}, [products]); // Simple calculation, useMemo adds overhead
// After: direct calculation
function ProductList({ products }) {
const productCount = products.length; // Simple enough
}
}
// Good useMemo: expensive calculation
function Chart({ data, complexConfig }) {
const processedData = useMemo(() => {
return expensiveDataTransformation(data, complexConfig);
}, [data, complexConfig]); // ✅ Worth memoizing
}Rule of thumb: Only use useMemo for calculations that are noticeably expensive (like sorting large arrays, complex transformations, or heavy computations).
Check your code editor and build output to confirm the ESLint warning is gone.
# Run ESLint to check for warnings
npm run lint
# Or build the project
npm run buildThe warning should no longer appear. Test that your memoized values update correctly by changing dependencies and verifying the calculation re-runs as expected.
Understanding useMemo vs useCallback: useMemo memoizes a value, while useCallback memoizes a function. Both use dependency arrays and follow the same dependency rules.
Shallow comparison of dependencies: React uses Object.is to compare dependency values. For objects and arrays, this means reference equality, not deep equality.
When to disable the rule: In rare cases with intentionally stable dependencies, you might disable the rule:
const sortedItems = useMemo(() => {
return items.sort(ALWAYS_THE_SAME_COMPARATOR);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]); // Comparator never changesPerformance considerations: useMemo adds memory overhead (storing the cached value) and comparison overhead (checking dependencies). Only use it when the calculation cost exceeds these overheads.
Common patterns that trigger warnings:
- Using array methods: items.map(x => transform(x, config)) needs config
- Object destructuring: const { a, b } = props needs props or a, b
- Function calls: calculate(items, multiplier) needs both
React 18+ concurrent features: With concurrent rendering, stale memoized values can cause tearing (inconsistent UI). Always include dependencies to ensure values stay synchronized with the latest render.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite