# React Hooks Masterclass: useState, useEffect, and Custom Hooks

* * *

> How does React remember information between renders? The answer is hooks — and understanding them deeply changes how you think about building components, not just how you write them.

* * *

## Table of Contents

1.  [Why React Hooks Were Introduced](#1-why-react-hooks-were-introduced)
    
2.  [Understanding useState](#2-understanding-usestate)
    
3.  [Understanding React Re-renders](#3-understanding-react-re-renders)
    
4.  [Understanding useEffect](#4-understanding-useeffect)
    
5.  [Dependency Arrays Explained](#5-dependency-arrays-explained)
    
6.  [Common useEffect Patterns](#6-common-useeffect-patterns)
    
7.  [Custom Hooks](#7-custom-hooks)
    
8.  [When to Create Custom Hooks](#8-when-to-create-custom-hooks)
    
9.  [Rules of Hooks and Best Practices](#9-rules-of-hooks-and-best-practices)
    
10.  [Thinking in Hooks](#10-thinking-in-hooks)
     

* * *

## 1\. Why React Hooks Were Introduced

Before hooks, React had two kinds of components: class components (which could have state and lifecycle methods) and function components (which were just for display — no state, no side effects).

If a component needed to track state or fetch data, it had to be a class:

```javascript
// Pre-hooks — a class component to fetch and display a user
class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    this.state = { user: null, loading: true };
  }

  componentDidMount() {
    fetch(`/api/users/${this.props.userId}`)
      .then(res => res.json())
      .then(user => this.setState({ user, loading: false }));
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.setState({ loading: true });
      fetch(`/api/users/${this.props.userId}`)
        .then(res => res.json())
        .then(user => this.setState({ user, loading: false }));
    }
  }

  render() {
    if (this.state.loading) return <p>Loading...</p>;
    return <h1>{this.state.user.name}</h1>;
  }
}
```

This has three problems. The data-fetching logic is split across `componentDidMount` and `componentDidUpdate` — two lifecycle methods — even though it's the same operation. It can't be extracted and reused in another component without copying the class. And the `this` keyword creates confusion that even experienced developers stumble on.

Hooks were introduced in React 16.8 (2019) to solve these three problems simultaneously:

1.  Related logic stays together, not split across lifecycle methods
    
2.  Stateful logic can be extracted into reusable functions (custom hooks)
    
3.  Function components can do everything class components could — no `this` required
    

```jsx
// With hooks — the same component, dramatically simpler
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(user => {
        setUser(user);
        setLoading(false);
      });
  }, [userId]);

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

The fetch logic is in one place, not split across two lifecycle methods. If this logic needs to be reused, it can be extracted into a `useUser` custom hook. And there's no class, no constructor, no `this`.

Hooks are functions that start with `use`. They let function components opt into React features — state, side effects, context — that previously required class components.

* * *

## 2\. Understanding useState

Every render of a React component is a fresh function call. Variables declared inside the component function are created anew on each render. So how does a component remember what the user typed, how many items are in the cart, or whether a modal is open?

It remembers because React stores state *outside* the component function, in a stable location that persists between renders. `useState` is the hook that connects your component to that storage.

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

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑             ↑           ↑
  //  current value  setter fn   initial value (used only on first render)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
}
```

`useState(0)` returns an array of two items — the current value and a function to update it. Destructuring gives them names. The names are yours to choose; React doesn't care what you call them as long as they're destructured in the right order.

### What happens when you call the setter

```plaintext
User clicks + button
    │
    ▼
setCount(count + 1) is called
    │
    ▼
React queues a state update: "count should be 1"
    │
    ▼
React re-renders the Counter component
    │
    ▼
useState(0) is called again — but React ignores the 0 (already initialised)
React returns the new stored value: 1
    │
    ▼
count is now 1 inside this render
UI reflects: "Count: 1"
```

The initial value — the `0` in `useState(0)` — is used exactly once: on the very first render. On every subsequent render, React returns the stored value, not the initial value.

### Multiple state variables

One component can have as many `useState` calls as it needs. Each one is independent — updating one doesn't affect the others.

```jsx
function ProfileEditor() {
  const [name, setName] = useState("Priya");
  const [bio, setBio] = useState("");
  const [isEditing, setIsEditing] = useState(false);
  const [saved, setSaved] = useState(false);

  function handleSave() {
    // save to API...
    setSaved(true);
    setIsEditing(false);
    setTimeout(() => setSaved(false), 3000);
  }

  return (
    <div>
      {isEditing ? (
        <>
          <input value={name} onChange={e => setName(e.target.value)} />
          <textarea value={bio} onChange={e => setBio(e.target.value)} />
          <button onClick={handleSave}>Save</button>
        </>
      ) : (
        <>
          <h2>{name}</h2>
          <p>{bio}</p>
          <button onClick={() => setIsEditing(true)}>Edit</button>
        </>
      )}
      {saved && <p className="success">Saved!</p>}
    </div>
  );
}
```

Four separate pieces of state, each independent. `isEditing` controls which view is shown. `name` and `bio` hold the form values. `saved` shows the success message temporarily.

### Updating state based on previous state

When the new state depends on the old state, use the functional form of the setter. This guarantees you're working with the most recent value, even if multiple updates are batched together.

```jsx
// Potentially stale — uses count from the current render's closure
setCount(count + 1);

// Safe — React passes the latest state value to the function
setCount(prevCount => prevCount + 1);
```

The functional form matters most when you call the setter multiple times in rapid succession, or when the setter is called inside an event handler or async function where the `count` variable might be stale.

### Grouping related state

When multiple state values always update together, consider grouping them into one object. This reduces the number of individual setter calls and makes the relationship explicit.

```jsx
// Three separate state variables — always updated together
const [lat, setLat] = useState(0);
const [lng, setLng] = useState(0);
const [zoom, setZoom] = useState(10);

// Better — one object, one setter, one conceptual unit
const [mapView, setMapView] = useState({ lat: 0, lng: 0, zoom: 10 });

// Spread to update only one field
setMapView(prev => ({ ...prev, zoom: prev.zoom + 1 }));
```

* * *

## 3\. Understanding React Re-renders

A component re-renders when its state changes. Understanding exactly what "re-render" means — and what it costs — prevents a whole class of performance misconceptions.

When a component re-renders, React calls the function again, from top to bottom. Every line of code in the component body runs again. Variables are re-declared. Expressions are re-evaluated. JSX is produced again.

React then compares the new output to the previous output (the virtual DOM diffing process) and updates only the real DOM nodes that actually changed.

```plaintext
Initial render of Counter:
  count = 0
  Component runs → produces: <button>-</button> <p>Count: 0</p> <button>+</button>
  React creates DOM nodes

User clicks +:
  setCount(1) called
  React re-renders Counter
  count = 1
  Component runs again → produces: <button>-</button> <p>Count: 1</p> <button>+</button>
  React diffs: only the text inside <p> changed
  React updates: just that text node in the real DOM
  Buttons are unchanged — React leaves them alone
```

### What triggers a re-render

```plaintext
1. State changes         → setCount(), setUser(), etc.
2. Props change          → parent passes a different value
3. Context changes       → a consumed context updates
4. Parent re-renders     → children re-render by default
```

Re-renders are not expensive by default. The component function runs — plain JavaScript. The expensive part is updating the real DOM, and React minimises that through diffing. Most re-render performance problems are structural (unnecessary state at too high a level) rather than something React.memo can solve.

### Avoid storing derivable data in state

A common mistake: putting data in state that can be computed from other state or props. This creates synchronisation problems and unnecessary re-renders.

```jsx
// WRONG — itemCount is derived from items, storing it separately
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);

function addItem(item) {
  setItems(prev => [...prev, item]);
  setItemCount(prev => prev + 1); // ← easy to forget this, easy to get out of sync
}

// CORRECT — derive itemCount during render
const [items, setItems] = useState([]);
const itemCount = items.length; // ← always accurate, zero maintenance

function addItem(item) {
  setItems(prev => [...prev, item]); // ← one update, both values stay in sync
}
```

The rule: if a value can be computed from existing state or props, compute it in the function body during render. Don't store it in state.

* * *

## 4\. Understanding useEffect

While `useState` manages data inside a component, `useEffect` manages everything that reaches *outside* the component — fetching data from an API, subscribing to browser events, starting a timer, updating the page title, connecting to a WebSocket.

These are called **side effects** — operations that affect something beyond the component's own rendered output.

The mental model for `useEffect` is not "run this after mount" or "run this when X changes." The better model is: **synchronise something external with the current state of your component**.

```jsx
useEffect(() => {
  // side effect code here — runs after render

  return () => {
    // cleanup code — runs before the next effect, or before unmount
  };
}, [/* dependencies */]);
```

### Why effects run after render, not during

React renders your component — produces the JSX output — then updates the DOM, then runs effects. This order ensures the DOM is ready before effects touch it, and that the component's render is never blocked by potentially slow operations like network requests.

```plaintext
Component renders
    │
    ▼
DOM updates
    │
    ▼
useEffect runs (after render is complete)
    │
    ▼
[state update inside effect?]
    │
    ▼
Component re-renders with new state
    │
    ▼
DOM updates again
    │
    ▼
useEffect runs again (if dependencies changed)
```

### A first example — updating the document title

```jsx
function ProfilePage({ user }) {
  useEffect(() => {
    document.title = `${user.name}'s Profile — MyApp`;

    return () => {
      document.title = "MyApp"; // reset when leaving the page
    };
  }, [user.name]); // re-run when user.name changes

  return <h1>{user.name}</h1>;
}
```

This synchronises `document.title` with `user.name`. Whenever `user.name` changes, the title updates. When the component unmounts (the user navigates away), the cleanup function resets the title.

* * *

## 5\. Dependency Arrays Explained

The dependency array is the third argument to `useEffect`. It controls when the effect runs. This is where most `useEffect` bugs come from, and where the deepest understanding is required.

### Three distinct forms

```jsx
// Form 1: No dependency array — runs after EVERY render
useEffect(() => {
  console.log("renders: ", count);
});

// Form 2: Empty dependency array — runs only ONCE, after the first render
useEffect(() => {
  console.log("mounted — runs once");
  return () => console.log("unmounting");
}, []);

// Form 3: With dependencies — runs after renders where a dependency changed
useEffect(() => {
  console.log("userId changed to:", userId);
}, [userId]);
```

### How React evaluates dependencies

React compares each dependency to its value from the previous render using `Object.is` — essentially strict equality (`===`). If any value changed, the effect runs again.

```jsx
function UserPage({ userId }) {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUserData(data));
  }, [userId]); // re-fetch only when userId changes, not on every render
}
```

The effect runs once on mount (when `userId` is first available), then again any time `userId` changes. It doesn't run when unrelated state in `UserPage` changes.

### The infinite loop — the most common useEffect bug

```jsx
// BROKEN — infinite loop
function BrokenComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/data")
      .then(res => res.json())
      .then(result => setData(result)); // ← setData causes a re-render
  }); // ← no dependency array → runs after EVERY render
  //                              setData → re-render → effect runs → setData → ...
}
```

The fix is the dependency array:

```jsx
// FIXED — runs once, on mount
useEffect(() => {
  fetch("/api/data")
    .then(res => res.json())
    .then(result => setData(result));
}, []); // ← empty array: run once, don't re-run
```

### Missing dependencies — the subtle bug

The opposite mistake: omitting dependencies to prevent re-runs, causing the effect to use stale values.

```jsx
// BROKEN — stale closure
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetch(`/api/search?q=${query}`) // ← uses query from the first render only
      .then(res => res.json())
      .then(setResults);
  }, []); // ← empty array — never re-runs, even when query changes

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}
```

Type a new search query — nothing happens. The effect captured the original `query` value and never updated.

```jsx
// FIXED — query is a dependency
useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResults);
}, [query]); // ← re-runs whenever query changes — correct
```

**The rule:** every value used inside `useEffect` that comes from the component (state, props, or anything derived from them) must be listed in the dependency array. The ESLint `exhaustive-deps` rule enforces this automatically.

### Cleanup functions

Some effects create resources that need to be released: event listeners, timers, subscriptions, WebSocket connections. The cleanup function returned from `useEffect` handles this.

```jsx
useEffect(() => {
  // Set up the effect
  const subscription = chatService.subscribe(roomId, handleMessage);

  // Return a cleanup function
  return () => {
    subscription.unsubscribe(); // run before the effect re-runs or before unmount
  };
}, [roomId]);
```

When `roomId` changes, React runs the cleanup (unsubscribes the old room), then runs the effect again (subscribes to the new room). When the component unmounts, React runs the cleanup one final time.

Without cleanup, subscribing to a new room doesn't unsubscribe from the old one. After navigating between enough rooms, you'd have dozens of active subscriptions, all processing the same messages.

* * *

## 6\. Common useEffect Patterns

### Data fetching with loading and error states

```jsx
function ArticlePage({ articleId }) {
  const [article, setArticle] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false; // prevents setting state on an unmounted component

    setLoading(true);
    setError(null);

    fetch(`/api/articles/${articleId}`)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setArticle(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true; // if articleId changes before fetch completes, ignore the result
    };
  }, [articleId]);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <Article data={article} />;
}
```

The `cancelled` flag handles a subtle race condition: if the user navigates to a different article before the first fetch completes, the first response arrives and calls `setArticle` on what might now be a different article's component. Setting `cancelled = true` in the cleanup prevents that stale update.

### Listening to browser events

```jsx
function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMouseMove(event) {
      setPosition({ x: event.clientX, y: event.clientY });
    }

    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove); // cleanup
    };
  }, []); // empty array — set up once, clean up on unmount

  return <p>Mouse: {position.x}, {position.y}</p>;
}
```

### Timers and intervals

```jsx
function Countdown({ seconds }) {
  const [remaining, setRemaining] = useState(seconds);

  useEffect(() => {
    if (remaining <= 0) return; // don't start a timer if already at zero

    const timer = setInterval(() => {
      setRemaining(prev => prev - 1);
    }, 1000);

    return () => clearInterval(timer); // cleanup when component unmounts or seconds changes
  }, [remaining]); // re-run when remaining changes (stops naturally at 0)

  return <p>Time remaining: {remaining}s</p>;
}
```

* * *

## 7\. Custom Hooks

A custom hook is a JavaScript function whose name starts with `use` and that can call other hooks inside it. That's the entire definition. There's no special API, no registration step, no ceremony.

Custom hooks exist to **extract and reuse stateful logic**. Any pattern you find yourself writing in multiple components — fetching a user, tracking window size, managing a form — can be extracted into a custom hook and shared.

### The problem without custom hooks

Imagine you need data-fetching logic in three components: `UserProfile`, `ProductPage`, and `OrderHistory`. Without custom hooks, you copy the pattern three times:

```jsx
// In UserProfile:
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
  fetch(`/api/users/${userId}`)...
}, [userId]);

// In ProductPage: (exact same structure, different URL)
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
  fetch(`/api/products/${productId}`)...
}, [productId]);

// In OrderHistory: (same again)
const [orders, setOrders] = useState(null);
...
```

Three copies of the same logic, three opportunities for the pattern to diverge, three places to fix any bug in the pattern.

### Extracting the logic into a custom hook

```jsx
// hooks/useFetch.js
import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;

    let cancelled = false;
    setLoading(true);
    setError(null);

    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;
```

Now all three components become simple:

```jsx
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <h1>{user.name}</h1>;
}

function ProductPage({ productId }) {
  const { data: product, loading, error } = useFetch(`/api/products/${productId}`);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <h2>{product.name} — ₹{product.price}</h2>;
}
```

The pattern is in one file. A bug fix, an improvement (adding retry logic, caching, cancellation), or a new feature (like abort controllers) benefits every consumer automatically.

### Each hook call has its own isolated state

An important subtlety: calling `useFetch` in two components gives each component its own independent state. The hook logic is shared; the state is not. This is exactly the same as calling `useState` twice in the same component — each call creates separate storage.

```jsx
// Two independent useFetch calls — separate data, separate loading states
const { data: user } = useFetch(`/api/users/${userId}`);
const { data: orders } = useFetch(`/api/orders?userId=${userId}`);
```

* * *

## 8\. When to Create Custom Hooks

Any time you notice yourself copying the same `useState`/`useEffect` pattern into more than one component, that's a signal to extract a custom hook. Here are the most common patterns.

### Theme and user preferences

```jsx
function useTheme() {
  const [theme, setTheme] = useState(
    () => localStorage.getItem("theme") || "light"
  );

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("theme", theme);
  }, [theme]);

  const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");

  return { theme, toggleTheme };
}

// Usage in any component:
function Header() {
  const { theme, toggleTheme } = useTheme();
  return (
    <header>
      <button onClick={toggleTheme}>
        {theme === "light" ? "🌙 Dark" : "☀️ Light"}
      </button>
    </header>
  );
}
```

### Window dimensions

```jsx
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }

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

  return size;
}

// Usage:
function ResponsiveLayout() {
  const { width } = useWindowSize();
  return width < 768 ? <MobileLayout /> : <DesktopLayout />;
}
```

### Debounced value — for search inputs

```jsx
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer); // cancel the timer if value changes before delay
  }, [value, delay]);

  return debouncedValue;
}

// Usage — only fires search after user stops typing for 500ms
function SearchBar() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 500);
  const { data: results } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  );

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {results?.map(r => <SearchResult key={r.id} result={r} />)}
    </div>
  );
}
```

### Local storage persistence

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

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

  return [value, setValue];
}

// Replaces useState — automatically persists to localStorage
function Settings() {
  const [language, setLanguage] = useLocalStorage("language", "en");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 16);

  return (
    <div>
      <select value={language} onChange={e => setLanguage(e.target.value)}>
        <option value="en">English</option>
        <option value="hi">Hindi</option>
      </select>
    </div>
  );
}
```

* * *

## 9\. Rules of Hooks and Best Practices

React hooks have two hard rules. They exist because React relies on the order that hooks are called to associate each hook with its stored state. Break the rules and the association breaks.

### Rule 1 — Only call hooks at the top level

Never call hooks inside conditions, loops, or nested functions. Hooks must be called unconditionally, in the same order, on every render.

```jsx
// WRONG — hook inside a condition
function Profile({ userId, isLoggedIn }) {
  if (isLoggedIn) {
    const [user, setUser] = useState(null); // ← conditional hook — breaks rules
  }
}

// CORRECT — hook always called, condition inside
function Profile({ userId, isLoggedIn }) {
  const [user, setUser] = useState(null);
  // use the isLoggedIn condition inside the effect or in the JSX
  useEffect(() => {
    if (isLoggedIn) {
      fetch(`/api/users/${userId}`)...
    }
  }, [userId, isLoggedIn]);
}
```

### Rule 2 — Only call hooks from React functions

Hooks can only be called from React function components or other custom hooks. Not from regular JavaScript functions, class components, or event handlers.

```jsx
// WRONG — calling a hook from a regular function
function someUtilityFunction() {
  const [count] = useState(0); // ← not a React function — error
}

// CORRECT — hooks only in components and custom hooks
function Counter() {
  const [count, setCount] = useState(0); // ← inside a component — correct
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```

The `eslint-plugin-react-hooks` package enforces both rules automatically. Install it and let the linter catch violations rather than relying on memory.

### Organising hooks in a component

Group related state together. Put all hooks at the top of the component, before any conditional logic or early returns. Use custom hooks to keep components readable.

```jsx
function ProductDashboard({ productId }) {
  // ── State ──────────────────────────────────────
  const [activeTab, setActiveTab] = useState("overview");

  // ── Data fetching ───────────────────────────────
  const { data: product, loading, error } = useFetch(`/api/products/${productId}`);
  const { data: reviews } = useFetch(`/api/products/${productId}/reviews`);

  // ── Derived values ──────────────────────────────
  const averageRating = reviews?.reduce((sum, r) => sum + r.rating, 0) / reviews?.length;

  // ── Early returns ───────────────────────────────
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;

  // ── Render ──────────────────────────────────────
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Average rating: {averageRating?.toFixed(1)} ★</p>
      {/* tabs... */}
    </div>
  );
}
```

All hooks at the top. Derived values after hooks. Early returns after derived values. Render at the bottom. This structure is readable at a glance and never violates hook rules.

* * *

## 10\. Thinking in Hooks

The deepest shift that hooks enable is in how you think about component logic. Instead of "what lifecycle event should I use for this?" you ask two questions:

**"What data does this component need to display?"** That's state and props, managed with `useState` and received from parents.

**"What does this component need to stay synchronised with?"** That's effects, managed with `useEffect`. Each effect maintains synchronisation between your component's state and something external — the document title, a remote API, a browser event listener, a WebSocket connection.

```plaintext
Component State (useState)
      │
      │ describes the current state of the world
      ▼
  Render — UI = f(state, props)
      │
      │ after render, synchronise external things
      ▼
  useEffect — keep external world in sync with component state
      │
      │ external events update state
      ▼
  State updates → re-render → UI reflects new reality
```

### Effects are synchronisation, not events

The common misconception is that `useEffect` is React's version of "do this once when the component mounts." Sometimes that's what it does — but that's a consequence, not a definition.

The correct mental model: `useEffect` keeps something external synchronised with your component's current state. When the component mounts, it synchronises for the first time. When dependencies change, it synchronises again. When the component unmounts, it cleans up the synchronisation.

```jsx
// Thinking: "fetch when mounted and when userId changes"
// This is an events-based mental model — close, but incomplete

// Better thinking: "keep fetched data synchronised with userId"
// When userId is X, data should match what /api/users/X returns
// Effect ensures this is true after every render where userId changed
useEffect(() => {
  fetch(`/api/users/${userId}`)...
}, [userId]);
```

The synchronisation framing makes the dependency array intuitive: "what does this synchronisation depend on?" The answer goes in the array.

### Custom hooks are units of reusable logic

Think of custom hooks as the equivalent of utility functions — except they can use state and effects. Just as you extract a `formatDate()` utility when you use it in multiple places, extract a `useUserData()` hook when you use that data-fetching pattern in multiple components.

Custom hooks compose naturally:

```jsx
// Each hook does one thing
function useUser(userId) {
  return useFetch(`/api/users/${userId}`);
}

function useUserPosts(userId) {
  return useFetch(`/api/users/${userId}/posts`);
}

// Compose them in a component
function UserPage({ userId }) {
  const { data: user } = useUser(userId);
  const { data: posts } = useUserPosts(userId);
  const { width } = useWindowSize();
  const { theme } = useTheme();

  // Component body is clean — logic lives in hooks
}
```

The component is a thin layer that composes hooks and renders JSX. The logic is in the hooks, testable and reusable independently.

* * *

## Quick Recap

```plaintext
Why hooks:
  Class components split related logic across lifecycle methods
  Logic couldn't be reused across components without copying
  Hooks: stateful logic in function components, extractable and shareable

useState:
  const [value, setValue] = useState(initialValue)
  Initial value used once — on first render only
  Calling setValue triggers a re-render
  Use functional form when new state depends on old: setValue(prev => prev + 1)
  Don't store derived values — compute them during render

useEffect:
  Runs after render, not during
  Three forms:
    useEffect(fn)        → runs after every render
    useEffect(fn, [])    → runs once, after first render
    useEffect(fn, [dep]) → runs when dep changes
  Return a cleanup function for subscriptions, timers, listeners
  Every value from the component used inside the effect goes in the array

Common mistakes:
  No dep array → infinite loop if effect updates state
  Empty dep array + stale value → effect uses outdated data
  Missing cleanup → memory leaks, duplicate subscriptions

Custom hooks:
  Function starting with "use" that calls other hooks
  Extracts and shares stateful logic across components
  Each call creates independent state — shared logic, not shared data
  Build for: data fetching, browser APIs, persisted preferences, timers

Rules of hooks:
  Only call at the top level — never in conditions or loops
  Only call from React functions — never from plain JS functions
  Use eslint-plugin-react-hooks to enforce automatically

Mental model:
  useState: what data does this component display?
  useEffect: what does this component need to stay synchronised with?
  Custom hooks: what logic do multiple components share?
```

* * *

Hooks are not a collection of APIs to memorise — they're a way of thinking about what a component does and what it depends on. `useState` is memory between renders. `useEffect` is synchronisation with the outside world. Custom hooks are reusable units of that logic. Once those three ideas are solid, every hook you encounter is just a variation of one of them.

* * *

*Found this useful? Drop a reaction and share it with someone working through React hooks. Got a question about a specific hook pattern? Leave a comment — I read every one.*
