Chapter 40: Removing Effect Dependencies
Core Idea
A "fix" that involves removing or suppressing a dependency the linter flags is almost never right — the correct fix is nearly always to change the Effect's code so that value genuinely doesn't need to be a dependency, not to hide the dependency from the array.
Key Concepts
- Dependencies should match the code, always — the array isn't a knob you tune for desired timing; it's a mechanical consequence of what the Effect body reads. If you want an Effect to re-run less often, the fix is to make the Effect body read fewer reactive values, not to omit values it does read from the array.
- Do you need this dependency at all? Common reasons a value ends up an "unwanted" dependency: it's actually an Effect Event candidate (Ch 39 — reads latest value without needing reactivity), it's an object/function recreated fresh every render (see below), or the Effect is doing two unrelated things that should be split into two Effects with different dependency sets.
- Objects and functions as dependencies are a frequent trap: an object or function literal created inside the component body is a new reference on every render, so
[options] where options = { roomId } re-triggers the Effect every render even though the meaningful content didn't change. Fixes: move the object/function creation inside the Effect itself if nothing else needs it, or depend on the primitive values (roomId) instead of the wrapper object.
- Wrap a function passed as a dependency in
useEffectEvent if it needs to read the latest props/state without itself becoming a reactive dependency (this is exactly Ch 39's pattern, applied here as a dependency-removal technique).
- If an Effect needs a value only to decide "did this actually change," compare against a ref inside the Effect rather than adding the value as a dependency that would trigger a full re-run on every change.
Code Examples
// ❌ `options` is a new object every render → Effect re-runs every render
const options = { serverUrl, roomId };
useEffect(() => { /* connect using options */ }, [options]);
// ✅ Depend on the primitive values the Effect actually needs
useEffect(() => {
const options = { serverUrl, roomId }; // created fresh inside the Effect, fine
/* connect using options */
}, [serverUrl, roomId]);
- What it demonstrates: moving object construction inside the Effect body so the dependency array can list the underlying primitives instead of an unstable wrapper reference.
Key Takeaways
- Never suppress the exhaustive-deps lint rule to "fix" an unwanted re-run — treat the warning as correct and change the code instead.
- Objects/functions built fresh in the component body are the single most common source of unnecessary Effect re-runs — depend on their primitive contents, not the wrapper reference.
- If an Effect seems to need conflicting dependency behavior (react to X, but not to Y even though Y is read), that's usually the Effect Event pattern from Ch 39, not a dependency-array workaround.
Connects To
- Ch 38 (Lifecycle of Reactive Effects): the "dependencies mirror what the body reads" rule this chapter operationalizes.
- Ch 39 (Separating Events from Effects): the primary tool for values that must stay current without being reactive.
- Ch 47 (ESLint Rules Reference): the
exhaustive-deps rule that catches dependency-array mistakes automatically.