Effect Separation: One Effect per Concern
Split effects so each handles a single job with its own dependencies.
I had one useEffect that set the document title from name and logged every count change. The dependency array was [name, count], so typing in the name field fired the count log too.
Two Jobs, One Dependency Array
import { useState, useEffect } from "react";
function App() {
const [name, setName] = useState("John");
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Hello ${name}`;
console.log(`Count changed to: ${count}`);
}, [name, count]);
return (
<div>
<h1>Effect Separation Pattern</h1>
<div>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
</div>
);
}
export default App;
Neither job cares about the other’s data, but sharing an effect ties their lifecycles together and muddies the dependency array. Every re-run does both pieces of work whether it needed to or not.
Split Them
The fix is to give each job its own effect, with only the dependencies it actually reads.
import { useState, useEffect } from "react";
function App() {
const [name, setName] = useState("John");
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Hello ${name}`;
}, [name]);
useEffect(() => {
console.log(`Count changed to: ${count}`);
}, [count]);
return (
<div>
<h1>Effect Separation Pattern</h1>
<div>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
</div>
);
}
export default App;
Now the title effect re-runs only when name changes, and the logger only when count does. Each dependency array finally describes its own effect.
One effect, one reason to re-run. When an effect keeps growing, I pull logic out into helpers and keep the body small. And before writing an effect at all, I check whether the value could be derived during render or moved into an event handler instead.