# useEffect — Part 1: The Core API

## 1. The Basic Signature

`useEffect` lets a component run code that reaches *outside* the render — touching the DOM, a subscription, a timer, a network request — after React has committed the render's output to the screen.

```tsx
import { useEffect, useState } from "react";

function DocumentTitle({ title }: { title: string }) {
  useEffect(() => {
    document.title = title; // side effect: mutating something outside React's own tree
  }, [title]);

  return null;
}
```

`useEffect(setup, deps)` takes a setup function and a dependency array. Critically, the setup function runs **after** the browser has painted the new UI, and it runs **asynchronously** — never during render, and never blocking the paint the user sees. Render must stay a pure computation of "props/state → JSX"; anything that isn't that computation belongs in an effect (or an event handler — see section 5 and the Common Pitfalls in Part 2).

## 2. The Dependency Array's Three Forms

```tsx
function Example({ id }: { id: number }) {
  useEffect(() => {
    console.log("runs after EVERY render, no array at all");
  });

  useEffect(() => {
    console.log("runs ONCE, only after the first render (mount)");
  }, []);

  useEffect(() => {
    console.log("runs after mount, and again whenever `id` changes");
  }, [id]);
}
```

| Form | When it runs |
|---|---|
| *(omitted)* | After every render, no exceptions |
| `[]` | Once, after the initial mount only |
| `[a, b]` | After mount, then again whenever `a` or `b` differs from its previous value |

An omitted array is rarely what you want — it's usually a sign the effect should either depend on specific values (`[a, b]`) or run once (`[]`). Section 5 (and Part 2's pitfalls) covers what "differs" actually means for objects and functions.

## 3. Cleanup Functions

If the setup function returns a function, React treats that as **cleanup** — it runs before the effect runs again (right before the *next* execution) and on unmount.

```tsx
function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize); // cleanup: undo exactly what setup did
    };
  }, []);

  return <p>{width}px</p>;
}
```

Every subscription-style effect (event listeners, `setInterval`/`setTimeout`, WebSocket connections, third-party library instances) should return a cleanup function that undoes it — otherwise every mount/re-run leaks another listener, another timer, another connection.

```tsx
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(id); // stop the OLD interval before any new one starts
  }, []);

  return <p>{seconds}s</p>;
}
```

## 4. Data Fetching With a Stale-Request Guard

A fetch effect that depends on a changing value (like an `id` prop) has a race condition: if `id` changes quickly, an earlier, slower request can resolve *after* a later, faster one — and overwrite the correct result with a stale one. The fix is a flag (or an `AbortController`) checked before committing the result:

```tsx
function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<{ name: string } | null>(null);

  useEffect(() => {
    let ignore = false; // flips to true if this effect is cleaned up before the fetch resolves

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!ignore) setUser(data); // only commit if this is still the LATEST request
      });

    return () => {
      ignore = true; // a newer effect run (new userId) invalidates this in-flight request
    };
  }, [userId]);

  return <p>{user?.name ?? "Loading..."}</p>;
}
```

An `AbortController` does the same job and additionally cancels the underlying network request instead of just ignoring its result:

```tsx
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== "AbortError") throw err; // an abort is expected, not a real failure
    });
  return () => controller.abort();
}, [userId]);
```

## 5. Gotcha: Object/Function Dependencies

Dependency comparison uses `Object.is` on each array element — for objects and functions, that means **reference** equality, not deep equality. An inline object or function literal creates a brand-new reference on every render, so an effect depending on one never sees "unchanged":

```tsx
function Broken({ userId }: { userId: number }) {
  const options = { userId }; // NEW object reference every render

  useEffect(() => {
    fetchData(options);
  }, [options]); // "changed" on every render — options is never `Object.is`-equal to last render's
  // → this effect runs after every single render, not just when userId changes
}
```

The fix is either depending on the primitive values directly (`[userId]` instead of `[options]`), or memoizing the object with `useMemo` so its reference is stable across renders when its contents haven't changed (its own topic — this exact problem is `useMemo`'s primary motivation).

Continue to `react_useeffect_theory_2.md` for effect timing, StrictMode's double-invoke behavior, and the fiber-level mechanics of how effects are actually scheduled and diffed.
