Back to all notes

Effect Separation: One Effect per Concern

Split effects so each handles a single job with its own dependencies.

2 min read

Introduction

Effect separation is about giving each useEffect a single responsibility. Instead of one big effect doing multiple things, you split them so each has a clear purpose and its own dependency list.

Why this matters

Bundling unrelated work into one useEffect ties their lifecycles together and leads to confusing dependency arrays. Separate effects make intent clear and avoid accidental re‑runs.

The problem

A single effect updates the document title and logs count changes, so both concerns re-run whenever either dependency changes.

Inefficient approach

Mixed concerns and coupled dependencies:

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;

The solution

One effect updates the title; another logs count changes.

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;

Step-by-step

  1. Identify distinct concerns inside the combined effect.

  2. Move each concern into its own useEffect.

  3. Give each effect the minimal dependency array it needs.

  4. Keep effect bodies small; extract helpers when logic grows.

Tips

  • Effects should be cohesive: one effect, one reason to re‑run.

  • Prefer deriving values or moving logic to event handlers when possible.

  • If an effect grows large, extract functions; keep the effect body small.

Knowledge base

Problem snippet:

useEffect(() => {
  document.title = `Hello ${name}`;
  console.log(`Count changed to: ${count}`);
}, [name, count]);

Solution snippet:

useEffect(() => { document.title = `Hello ${name}`; }, [name]);
useEffect(() => { console.log(`Count changed to: ${count}`); }, [count]);