ESLint's exhaustive-deps rule warns when a ref created with useRef is used inside useEffect but not listed in the dependency array. However, refs are intentionally stable and should typically not be included as dependencies since mutating ref.current does not trigger re-renders. Understanding when to include or exclude refs prevents confusion and maintains proper hook behavior.
This ESLint warning comes from the react-hooks/exhaustive-deps rule, which validates that all values used inside useEffect, useCallback, useMemo, and other hooks are properly listed in their dependency arrays. The rule helps prevent stale closure bugs by ensuring hooks re-run when their dependencies change. However, when it flags useRef, it's often a false positive or indicates a misunderstanding of how refs work. Refs created by useRef return a stable object that persists across renders - the ref object itself never changes, only its .current property can be mutated. Since mutating ref.current does not trigger re-renders, including it in a dependency array serves no functional purpose. ESLint may warn about this because it cannot always distinguish between refs and other values that should trigger re-execution.
The ref object returned by useRef never changes between renders - only ref.current can be mutated. Since mutations to ref.current do not trigger re-renders, including the ref in a dependency array does nothing. React's design intentionally makes refs stable so they can be used to store mutable values that persist without causing re-renders.
function MyComponent() {
const countRef = useRef(0);
useEffect(() => {
// Using countRef here - should it be a dependency?
countRef.current += 1;
console.log('Render count:', countRef.current);
});
// countRef object never changes, so no need to list it as a dependency
}You have two valid options: (1) Include the ref in the dependency array. This satisfies ESLint and is harmless since the ref is stable - it won't cause extra re-runs. (2) Add an ESLint disable comment if you're confident the ref doesn't need to be tracked.
// Option 1: Include the ref (harmless, satisfies ESLint)
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, [inputRef]); // Including ref is safe and silences the warning
}
// Option 2: Suppress the warning (use sparingly)
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Explicitly stating empty deps is intentional
}If you actually need your effect to re-run when a value changes, don't use useRef - use useState or useReducer instead. The fundamental purpose of refs is to store values that can change without triggering re-renders. If you need re-renders, you need state, not a ref.
// WRONG - trying to react to ref changes
function MyComponent() {
const valueRef = useRef(0);
useEffect(() => {
console.log('Value changed:', valueRef.current);
}, [valueRef.current]); // This won't work as expected!
const increment = () => {
valueRef.current += 1; // Mutation doesn't trigger re-render or effect
};
}
// CORRECT - use state if you need to react to changes
function MyComponent() {
const [value, setValue] = useState(0);
useEffect(() => {
console.log('Value changed:', value);
}, [value]); // Effect properly runs on value changes
const increment = () => {
setValue(v => v + 1); // State update triggers re-render and effect
};
}When using refs to access DOM elements, the ref object itself is stable. You can safely access ref.current in effects without listing the ref as a dependency, as long as you handle the case where ref.current might be null.
function MyComponent() {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Access DOM element through ref
if (divRef.current) {
const width = divRef.current.offsetWidth;
console.log('Div width:', width);
}
// No need to list divRef as a dependency
}, []);
return <div ref={divRef}>Content</div>;
}If you've created custom hooks that accept refs as parameters, you may need to configure ESLint to understand them. Use the additionalHooks option in your ESLint config to teach the exhaustive-deps rule about your custom hooks.
// .eslintrc.json
{
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "(useMyCustomHook|useAnotherHook)"
}
]
}
}In rare cases, ESLint might be correctly warning you about a potential issue. Review your code: Are you conditionally creating the ref? Are you passing refs from props that might change? Are you using ref.current in a way that expects fresh values each render? If any of these apply, reconsider your approach.
// SUSPICIOUS - ref from props might actually change
function MyComponent({ externalRef }: { externalRef: RefObject<HTMLElement> }) {
useEffect(() => {
externalRef.current?.focus();
}, []); // Should externalRef be a dependency? Probably yes!
}
// BETTER - include prop refs as dependencies
function MyComponent({ externalRef }: { externalRef: RefObject<HTMLElement> }) {
useEffect(() => {
externalRef.current?.focus();
}, [externalRef]); // Safer to include refs from props
}The distinction between stable values (like refs) and reactive values (like state) is fundamental to React's mental model. Kent C. Dodds explains that you shouldn't put refs in dependency arrays because "it's an indication that you want the effect callback to re-run when a value is changed, but you're not notifying React when that change happens." If you find yourself wanting an effect to re-run when ref.current changes, you're misusing refs - switch to useState. The exhaustive-deps rule is valuable for preventing stale closure bugs, but it has limitations: it cannot perform deep type analysis to determine if a value is stable, so it conservatively warns about everything. For refs passed as props, including them in the dependency array is safer because the prop itself might change to point to a different ref object (though this is rare). Some teams configure their ESLint to auto-fix this by always including refs, accepting that it's technically unnecessary but consistent. The React team is aware of these false positives (see GitHub issues #14920 and #23392) but hasn't implemented special handling because (1) including refs is harmless and (2) distinguishing refs from other values requires complex static analysis.
React.FC expects children prop to be defined
React.FC no longer includes implicit children prop
Warning: You provided a `selected` prop to a form field without an `onChange` handler
You provided a 'selected' prop without an onChange handler
Failed to load source map from suspense chunk
How to fix "Failed to load source map from suspense chunk" in React
Prop spreading could cause security issues
Prop spreading could cause security issues
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