# React Hooks Masterclass

### How Does React Remember Information Between Renders?

Here's a question that sounds simple but trips up most people learning React: when a component function runs top to bottom every time it renders, how does it "remember" anything? A regular JavaScript function forgets its local variables the moment it returns. So when you click a button and a counter goes from 3 to 4, where does that `3` live in between renders?

The answer is **hooks**. Hooks are React's mechanism for giving a function component memory (state) and a way to reach outside itself to talk to the world (effects). Once that clicks, almost everything else about hooks becomes a natural consequence of that one idea.

### Why React Hooks Were Introduced?

Before hooks (pre-2019), React components came in two flavors:

*   **Function components** — simple, but stateless. They could only render props; they couldn't hold their own data or react to lifecycle events.
    
*   **Class components** — could hold state (`this.state`) and tap into lifecycle methods (`componentDidMount`, `componentDidUpdate`, `componentWillUnmount`), but came with real costs.
    

### Problems with the class-based pattern

*   `this` **confusion.** Every method needed to be bound, or you'd reach for arrow-function class properties just to avoid `this` being `undefined`.
    
*   **Scattered logic.** Related logic (say, subscribing to a WebSocket) had to be split across `componentDidMount` (subscribe) and `componentWillUnmount` (unsubscribe), even though it's conceptually one piece of behavior. Meanwhile, unrelated logic piled up together inside the same lifecycle method.
    
*   **No easy way to reuse stateful logic.** If two components both needed "track window width" logic, your only real options were **higher-order components (HOCs)** or **render props** — both of which wrap your component tree in extra layers, making the component tree in React DevTools look like an onion, and making the code harder to trace.
    
*   **Large, hard-to-split components.** As a class component grew, it became one giant blob. There was no lightweight way to extract a slice of behavior without extracting an entire component.
    

### What hooks solved

*   **Reusing logic between components** — a custom hook lets you extract stateful logic into a plain function, no wrapping, no extra component layers.
    
*   **Simpler component development** — function components stay function components; you add capabilities by calling hooks, not by rewriting into a class.
    
*   **Grouping by concern, not by lifecycle** — code that belongs together (setup + cleanup for one feature) can live together in one `useEffect`, instead of being split across `componentDidMount` and `componentWillUnmount`.
    
*   **Modern React development** — hooks are now the default way to write React. Frameworks like Next.js, tooling like React DevTools, and most of the ecosystem (React Query, Zustand, Framer Motion) are designed hook-first.
    

The underlying shift: **React moved from "components are classes with lifecycle methods" to "components are functions with attached, composable capabilities."**

### Understanding useState

`useState` is the hook that gives a function component memory. It returns a pair: the current value, and a function to update it.

```javascript
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
```

*   `useState(0)` — `0` is the **initial value**, used only on the very first render.
    
*   `count` — the current state value for *this* render.
    
*   `setCount` — the function you call to request a new value and trigger a re-render.
    

**Updating state**

State updates are **requests**, not instant mutations. Calling `setCount(count + 1)` doesn't change `count` in the current render — it tells React "next time you render this component, use this new value."

A common trap:

```javascript
function handleTripleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}
```

You might expect `count` to jump by 3, but it only jumps by 1. Each call captures the *same* `count` from this render's closure. The fix is the **updater function** form, which receives the latest pending value:

```javascript
function handleTripleClick() {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
}
```

**Rule of thumb:** if your new state depends on the previous state, use the updater function form.

**Multiple state variables**

You're not limited to one `useState` call. Most components use several, each responsible for one independent piece of data:

```javascript
function UserProfile() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [isEditing, setIsEditing] = useState(false);

  // ...
}
```

Compare that to cramming everything into one object:

```javascript
const [profile, setProfile] = useState({ name: "", email: "", isEditing: false });
```

This works, but there's a catch: unlike class components' `this.setState`, `useState`'s setter **replaces** the state rather than merging it. So `setProfile({ isEditing: true })` would wipe out `name` and `email`. You'd need to manually spread the previous object:

```javascript
setProfile((prev) => ({ ...prev, isEditing: true }));
```

**Pattern:** use separate `useState` calls for independent values; group values into one state object only when they truly change together (e.g., `{ x, y }` coordinates).

### State-driven UI updates

The core idea of React is: **UI is a function of state.** You don't imperatively say "hide this div, show that div." You describe what the UI should look like *for a given state value*, and React figures out the DOM changes.

```javascript
function ThemeSwitcher() {
  const [theme, setTheme] = useState("light");

  return (
    <div className={theme === "dark" ? "app-dark" : "app-light"}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Switch to {theme === "light" ? "dark" : "light"} mode
      </button>
    </div>
  );
}
```

There's no manual DOM manipulation here — `theme` changes, the component re-renders, and the returned JSX simply looks different for the new state.

### Common state management patterns

*   **Toggle state:** `const [isOpen, setIsOpen] = useState(false)` with `setIsOpen((v) => !v)`.
    
*   **Form fields:** one `useState` per input, or a single object for a whole form.
    
*   **Derived values — don't store them.** If a value can be *calculated* from existing state, don't put it in its own `useState`. Compute it during render instead (more on this below).
    
*   **Lazy initial state:** if computing the initial value is expensive, pass a function instead of a value: `useState(() => expensiveComputation())`. This runs only once, not on every render.
    

### Understanding React Re-renders

This is the piece that ties everything together, so it's worth slowing down here.

**What triggers a re-render?**

A component re-renders when:

1.  **Its state changes** (a `useState` setter is called with a new value).
    
2.  **Its parent re-renders** (by default, children re-render too, even if their own props didn't change).
    
3.  **Context it subscribes to changes** (`useContext`).
    

Notably, **props changing isn't a separate trigger** — it's really a consequence of #2: the parent re-rendered and passed new props down.

**State updates and rendering:**

When you call a state setter:

1.  React schedules a re-render (it doesn't happen synchronously mid-function).
    
2.  React calls your component function again, top to bottom.
    
3.  Inside that new call, `useState` returns the *new* value.
    
4.  React compares the newly returned JSX against the previous render's output.
    
5.  React updates only the parts of the real DOM that actually changed.
    

**How React updates the UI?**

That comparison step is the "virtual DOM diffing" you may have heard about. Your component function returns a lightweight description of the UI (JSX → `React.createElement` calls), and React diffs that description against the previous one to compute the minimal set of real DOM operations. This is why React re-renders are relatively cheap — re-rendering a component doesn't mean "redraw everything on screen," it means "recompute the description and patch only the differences."

### Avoiding unnecessary state

A frequent beginner mistake is storing something in state that could just be computed during render:

```javascript
// Unnecessary state — fullName can drift out of sync
const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const [fullName, setFullName] = useState("Ada Lovelace");
```

```javascript
// Better — derive it, don't store it
const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const fullName = `${firstName} ${lastName}`;
```

**Rule of thumb:** if you can calculate a value from other state or props during render, it doesn't need its own `useState`. Extra state means extra ways for your UI to become inconsistent.

### Understanding useEffect

**Why useEffect exists?**

Rendering should be a pure calculation: given state and props, produce JSX. But real applications need to do things that aren't part of rendering — call an API, set up a subscription, manually interact with a browser API. These are **side effects**, and `useEffect` is React's designated place to run them, *after* the render has been committed to the screen.

```javascript
import { useEffect } from "react";

useEffect(() => {
  // side effect code
});
```

**Side effects in applications:**

Typical examples of side effects:

*   Fetching data from a server
    
*   Subscribing to a WebSocket or event emitter
    
*   Reading or writing to `localStorage`
    
*   Manually manipulating the DOM (measuring an element, focusing an input)
    
*   Setting up timers or intervals
    
*   Logging or analytics
    

**Data fetching:**

```javascript
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let ignore = false;

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!ignore) setUser(data);
      });

    return () => {
      ignore = true; // avoid setting state from a stale request
    };
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <h2>{user.name}</h2>;
}
```

The `ignore` flag matters: if `userId` changes quickly (e.g., navigating between profiles), an older fetch could resolve *after* a newer one and overwrite fresh data with stale data. The cleanup function prevents that.

**Event subscriptions:**

```javascript
useEffect(() => {
  function handleResize() {
    console.log(window.innerWidth);
  }

  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);
```

**Timers and intervals:**

```javascript
useEffect(() => {
  const id = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => clearInterval(id);
}, []);
```

**Cleanup functions:**

If a `useEffect` sets something up, it usually needs to tear it down — otherwise you get memory leaks, duplicate subscriptions, or timers that keep firing after a component is gone. The function you `return` from inside `useEffect` is that teardown, and React calls it:

*   right before the effect runs again (if dependencies changed), and
    
*   when the component unmounts.
    

```javascript
useEffect(() => {
  const controller = new AbortController();

  fetch("/api/data", { signal: controller.signal });

  return () => controller.abort(); // cleanup
}, []);
```

### Dependency Arrays Explained

**What dependency arrays are?**

The second argument to `useEffect` is an array of values. It tells React: "only re-run this effect if one of these values has changed since the last render."

```javascript
useEffect(() => {
  // effect body
}, [dependency1, dependency2]);
```

**Running effects once:**

An empty array means "this effect doesn't depend on anything from the render — run it only after the first render, never again":

```javascript
useEffect(() => {
  console.log("Component mounted");
}, []);
```

**Running effects on changes:**

Including a value means the effect re-runs whenever that value changes between renders:

```javascript
useEffect(() => {
  document.title = `${count} new messages`;
}, [count]);
```

**No dependency array at all:**

Omitting the array entirely means the effect runs after *every single render* — rarely what you want, but occasionally useful for debugging.

| Dependency array | When the effect runs |
| --- | --- |
| *(omitted)* | After every render |
| `[]` | Only after the first render |
| `[a, b]` | After the first render, and whenever `a` or `b` changes |

**Common dependency mistakes:**

*   **Omitting a value you actually use inside the effect.** If your effect reads `userId` but `[]` is the dependency array, the effect captures the *first* render's `userId` forever — a stale closure bug.
    
*   **Passing an object or function created fresh each render.** `useEffect(() => {...}, [options])` where `options = { limit: 10 }` is a new object literal every render — the effect will re-run every time, defeating the purpose of the dependency array.
    
*   **Fighting the linter instead of listening to it.** The `eslint-plugin-react-hooks` exhaustive-deps rule usually points at a genuine bug. Suppressing the warning is rarely the right fix — restructuring the effect usually is.
    

**Avoiding infinite loops:**

A classic beginner bug:

```javascript
const [data, setData] = useState([]);

useEffect(() => {
  setData([...data, "item"]); // depends on `data`, and changes `data`
}, [data]); // re-runs every time data changes → infinite loop
```

Fixes:

*   Use the updater form so the effect doesn't need `data` as a dependency: `setData((prev) => [...prev, "item"])`, and drop `data` from the array.
    
*   Or, more often, ask whether this really needs to be an effect at all — many "effects" that just recompute a value from existing state should instead be plain calculations during render.
    

### Common useEffect Patterns

**1\. Fetching data from APIs:**

The canonical pattern: fetch when a dependency (like an ID or search query) changes, guard against stale responses, and clean up.

```javascript
useEffect(() => {
  let cancelled = false;

  async function loadResults() {
    const res = await fetch(`/api/search?q=${query}`);
    const json = await res.json();
    if (!cancelled) setResults(json);
  }

  loadResults();
  return () => {
    cancelled = true;
  };
}, [query]);
```

**2\. Listening to browser events:**

Window resize, scroll position, online/offline status, keyboard shortcuts — all follow the subscribe-in-effect, unsubscribe-in-cleanup shape shown earlier.

**3\. Synchronizing external systems:**

This is arguably the best mental model for `useEffect`: it's not "component lifecycle code," it's **synchronization**. You're keeping something *outside* React (the DOM title, a chat server connection, a video player's play state) in sync with something *inside* React (props and state).

```javascript
useEffect(() => {
  const connection = createChatConnection(roomId);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]);
```

Whenever `roomId` changes, React disconnects the old connection and connects a new one — the external system stays synchronized with React's current state.

**Cleanup best practices:**

*   Always clean up anything that outlives a single render: subscriptions, timers, network requests, manual DOM listeners.
    
*   Cleanup functions should undo *exactly* what the effect set up — nothing more, nothing less.
    
*   If an effect doesn't set up anything persistent (e.g., a one-off `console.log`), it doesn't need a cleanup function at all.
    

### Lifecycle thinking in React

It's tempting to map `useEffect` onto the old class lifecycle: "`[]` is `componentDidMount`, cleanup is `componentWillUnmount`." That mapping isn't wrong, but it's the less useful way to think about it. **Lifecycle thinking asks "when does this run?" Synchronization thinking asks "what does this keep in sync, and with what?"** The second question leads to fewer bugs, because it forces you to name every dependency the synchronization actually relies on.

### Custom Hooks

**What are custom hooks?**

A custom hook is just a JavaScript function whose name starts with `use` and that calls other hooks inside it. That's the entire definition — there's no special API for "creating" a hook.

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

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

  return width;
}
```

Using it:

```javascript
function Sidebar() {
  const width = useWindowWidth();
  return <p>Window is {width}px wide</p>;
}
```

**Why custom hooks exist?**

Before hooks, sharing this "track window width" logic across `Sidebar` and, say, `Header`, meant wrapping both in a higher-order component or render-prop component. Custom hooks let you extract the *logic* without extracting or wrapping any *component*. Each component that calls `useWindowWidth()` gets its own independent state — the hook is reusable logic, not shared state.

**Reusing stateful logic:**

Custom hooks can bundle `useState` and `useEffect` together into a reusable named concept:

```javascript
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
```

```javascript
const [theme, setTheme] = useLocalStorage("theme", "light");
```

From the outside, `useLocalStorage` looks and behaves just like `useState` — but it also persists.

**Separating concerns:**

Custom hooks let a component's *rendering* logic stay clean while its *stateful* logic lives in a well-named, testable function elsewhere:

```javascript
function ProfilePage({ userId }) {
  const { user, loading, error } = useUser(userId);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <h1>{user.name}</h1>;
}
```

`ProfilePage` no longer needs to know *how* user data is fetched, cached, or retried — that complexity lives inside `useUser`.

**Building reusable abstractions:**

Good custom hooks read like a small vocabulary for your app's domain: `useAuth()`, `useCart()`, `useDebouncedValue()`, `useMediaQuery()`. Each hides its internal `useState`/`useEffect` machinery behind a clear name and a small, predictable return value.

### When to Create Custom Hooks?

You don't need a custom hook for every two lines of logic. Reach for one when you notice:

1.  **Shared business logic:** Two or more components need the exact same stateful behavior (form validation rules, permission checks, cart totals).
    
2.  **Data fetching logic:** Any time you find yourself copy-pasting a `useState` + `useEffect` fetch pattern between components, that's a strong signal to extract `useFetch(url)` or a resource-specific hook like `useUser(id)`.
    
3.  **Authentication logic:** Checking whether a user is logged in, exposing `login`/`logout` functions, and reading the current user typically belongs in a `useAuth()` hook (often built on top of context).
    

```javascript
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error("useAuth must be used within AuthProvider");
  return context;
}
```

1.  **Form management:** Tracking field values, validation errors, and submission state is repetitive enough across forms that a `useForm()` hook (or a library built on the same idea, like React Hook Form) pays for itself quickly.
    
2.  **Window and device utilities:** Window size, scroll position, online status, dark-mode preference, media query matches — all small, self-contained pieces of browser state that are natural custom-hook candidates:
    

```javascript
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function goOnline() { setIsOnline(true); }
    function goOffline() { setIsOnline(false); }
    window.addEventListener("online", goOnline);
    window.addEventListener("offline", goOffline);
    return () => {
      window.removeEventListener("online", goOnline);
      window.removeEventListener("offline", goOffline);
    };
  }, []);

  return isOnline;
}
```

### Hooks Rules and Best Practices

**Rules of hooks:**

*   **Only call hooks at the top level.** Never inside loops, conditions, or nested functions.
    
*   **Only call hooks from React functions.** Function components or other custom hooks — not regular JavaScript functions.
    

```javascript
// Wrong
if (isLoggedIn) {
  const [user, setUser] = useState(null);
}

// Right
const [user, setUser] = useState(null);
if (isLoggedIn) {
  // use `user` here
}
```

**Predictable execution:**

These rules exist because React tracks hooks **by call order**, not by name. On every render, React expects the first `useState` call to correspond to the same piece of state as the first `useState` call in the previous render, the second to match the second, and so on. If a hook call is skipped conditionally, every hook after it shifts by one — and React attaches the wrong state to the wrong hook.

**Organizing hooks:**

*   Group related `useState` declarations near the top of the component.
    
*   Keep each `useEffect` focused on one synchronization concern; don't cram unrelated side effects into a single effect just to "save a `useEffect` call."
    
*   Extract a hook as soon as logic needs to be reused, or as soon as a component's top feels cluttered with unrelated state and effects.
    

**Avoiding common mistakes:**

*   Don't call state setters directly during render (outside event handlers or effects) — that causes render loops.
    
*   Don't forget dependency arrays are about *values read inside the effect*, not about "when do I want this to run."
    
*   Don't overuse `useEffect` for things that are really just derived values or event-handler logic — not everything needs to be an effect.
    

**Building maintainable components:**

A useful gut check: if you can't explain what a `useEffect` synchronizes and with what, in one sentence, it likely needs to be split or rethought. Small, well-named hooks — built-in or custom — keep components readable months later.

### Summary

*   `useState` gives function components memory across renders.
    
*   **Re-renders** happen when state changes, and React recomputes the JSX description, diffing it against the previous render to patch the real DOM efficiently.
    
*   `useEffect` synchronizes your component with the outside world — after render, not during it.
    
*   **Dependency arrays** tell React exactly when that synchronization needs to re-run.
    
*   **Custom hooks** package reusable stateful logic into small, well-named functions — no wrapping, no extra component layers.
    

Master these five ideas, and the rest of the hooks API (`useReducer`, `useContext`, `useMemo`, `useCallback`, `useRef`) will feel like natural extensions of the same core model, rather than a new set of rules to memorize.
