You've been using a hook since lesson 02-04 without putting a name to the category: useState. And the last two lessons have surfaced the historical reasons hooks exist: the wrapper hell of HOCs and render props (04-02), and related logic scattered across lifecycle methods that sat fifty lines apart from each other (04-03). This lesson closes that loop. You'll understand exactly what a hook is, what problem it came to solve, why it has two apparently arbitrary rules of use — and the internal mechanism that explains them —, what hooks exist and which lesson in the course covers each one. We won't develop any of them in depth here: that's the job of the entire Module 5. Here we're laying out the map before we walk the territory.

Contents

  1. What problem hooks came to solve
  2. What a hook is, precisely
  3. Rule 1: only at the top level
  4. The mechanism: the ordered list of hooks
  5. Rule 2: only from components or from other hooks
  6. eslint-plugin-react-hooks, the safety net
  7. Catalogue of the course's hooks
  8. Combined basic use: SummaryPanel with useState + useEffect
  9. Local state, ref, and context: three different things

  1. What problem hooks came to solve

Before 2019, a React component had to pick between two incompatible shapes:

  • Function component: simple, readable, no this… but no state and no access to the lifecycle. Good only for presentation.
  • Class component: could do everything, at the cost of a constructor, bind, this, and lifecycle methods.

That forced a full refactor the moment a presentational component needed to remember a piece of data. But the deeper problem was something else, and worse: there was no decent way to reuse stateful logic.

Say three CicloUrbano components need to know the window's width to decide how many cards fit. With classes, the logic — subscribe to the resize event, store the width, unsubscribe — had to be copied into all three, or the three had to be wrapped in a HOC, or nested inside a render prop. The last two options produced trees like this one:

// What used to show up in React DevTools with four stacked HOCs
<withAuth(withTheme(withWidth(withLogging(OperatorPanel))))>
  <withTheme(withWidth(withLogging(OperatorPanel)))>
    <withWidth(withLogging(OperatorPanel))>
      <withLogging(OperatorPanel)>
        <OperatorPanel />   ← the only one that paints anything

Four levels that paint nothing, props that appear with no visible origin, and awkward debugging. That's the wrapper hell you saw in 04-02.

The second problem was organization by moment instead of by subject, which we analysed in 04-03:

componentDidMount() {
  this.loadBikes();                                       // concern A
  window.addEventListener('resize', this.measureWidth);   // concern B
  this.timer = setInterval(this.updateClock, 1000);        // concern C
}

componentWillUnmount() {
  window.removeEventListener('resize', this.measureWidth); // concern B, down here
  clearInterval(this.timer);                                // concern C, down here
}

Three concerns that have nothing to do with each other share a method, and each one has its other half in a different method. It's the perfect recipe for forgetting a cleanup.

Hooks solve all three at once:

Previous problem What hooks answer with
Functions couldn't have state useState and friends work in any function component
Reusing stateful logic required wrappers A custom hook is a function call, with no extra level in the tree
Related logic ended up scattered Each concern gets its own block, with its cleanup right next to it
this and bind Gone: there's no class

And one thing they didn't do: get rid of classes. They still work, and error boundaries still require them (04-05).

  1. What a hook is, precisely

A hook is a JavaScript function whose name starts with use and that hooks the component into React's internal system, giving it access to capabilities like state, effects, or context.

Let's unpack the definition:

  • It's an ordinary function. It isn't a language keyword, it isn't special syntax, it doesn't require compilation. useState is a function React exports and you import.
  • Its name starts with use. That's not decorative: it's the convention by which React and analysis tools recognize a hook. eslint-plugin-react-hooks decides whether a function is a hook by looking at its name, and applies the rules accordingly. If you write your own stateful function and call it getWidth instead of useWidth, the rules won't apply and you'll lose the safety net.
  • It hooks into the internal system. Here's the conceptual key. A function component is just a function: when it finishes, its variables disappear. So where does state get stored between renders? Outside the component, in an internal data structure of React's, tied to that specific instance. The hook is the cable that connects the function to that store.
function DockCounter() {
  const [docks, setDocks] = useState(20);
  // `docks` is a local variable that dies when the function finishes.
  // The VALUE 20 (and then 19, 18…) doesn't live here: it lives inside React.
  // useState is the cable that goes and fetches it on every render.
  return <p>Free docks: {docks}</p>;
}

That idea — state lives in React, not in the function — is what makes the two rules in the next section possible. It also explains something you already noticed in 02-04: two instances of the same component have independent states, because React keeps one store per instance, not per component.

  1. Rule 1: only at the top level

Call hooks only at the top level of the component. Never inside conditionals, loops, nested functions, or after an early return.

// CORRECT: all three hooks, at the top level, always in the same order
function AdvancedBookingPanel({ bike }) {
  const [hours, setHours] = useState(1);
  const [confirmed, setConfirmed] = useState(false);
  const reference = useRef(null);

  if (!bike) {
    return <p>Select a bike.</p>;   // the return comes AFTER the hooks
  }
  // …
}
// INCORRECT: the hook is inside an if
function AdvancedBookingPanel({ bike }) {
  if (bike) {
    const [hours, setHours] = useState(1);     // ❌
  }
  // …
}
// INCORRECT: early return BEFORE a hook
function AdvancedBookingPanel({ bike }) {
  const [hours, setHours] = useState(1);
  if (!bike) return null;                      // ❌ the next hook sometimes doesn't run
  const [confirmed, setConfirmed] = useState(false);
  // …
}
// INCORRECT: hook inside a loop
function CounterList({ stations }) {
  return stations.map((station) => {
    const [docks, setDocks] = useState(station.docks);   // ❌
    return <p key={station.id}>{docks}</p>;
  });
}

The last case has a conceptual fix, not a technical one: if each station needs its own state, each station needs its own component. You extract DockCounter and call the hook inside it. Each instance gets its own store, and the rule holds on its own.

Why such strict rules? Because React's internal mechanism depends on order. Let's see it.

  1. The mechanism: the ordered list of hooks

React doesn't store state by the variable's name. It can't: const [hours, setHours] is array destructuring, and React never sees the name hours. What it does is much simpler: for every component instance, it keeps an ordered list of cells, and a pointer that moves forward one position with every hook call.

flowchart LR
    subgraph R1["First render"]
      H1["useState(1)"] --> C1["cell 0<br/>value: 1"]
      H2["useState(false)"] --> C2["cell 1<br/>value: false"]
      H3["useRef(null)"] --> C3["cell 2<br/>value: {current: null}"]
    end
    subgraph R2["Subsequent renders"]
      S1["useState(1)"] --> D1["reads cell 0"]
      S2["useState(false)"] --> D2["reads cell 1"]
      S3["useRef(null)"] --> D3["reads cell 2"]
    end
    R1 --> R2

On the first render React creates the cells in the order the hooks are called. On every subsequent render it simply reads them in the same order. The link between useState(1) and its stored value is exclusively position.

That's where the rule comes from: if a render calls three hooks and the next one calls only two, the positions shift and every variable ends up with someone else's value.

The disaster, step by step

// BROKEN CODE, to understand the mechanism
function BookingPanel({ bike }) {
  const [hours, setHours] = useState(1);          // cell 0

  if (bike) {
    const [note, setNote] = useState('');         // ❌ cell 1 only sometimes
  }

  const [confirmed, setConfirmed] = useState(false);   // cell 1 or cell 2?
  // …
}
sequenceDiagram
    participant R1 as Render 1 (bike = object)
    participant M as React's cells
    participant R2 as Render 2 (bike = null)
    R1->>M: useState(1) → cell 0 = 1
    R1->>M: useState('') → cell 1 = ''
    R1->>M: useState(false) → cell 2 = false
    Note over M: [1, '', false]
    R2->>M: useState(1) → reads cell 0 = 1 ✅
    R2->>M: (the if doesn't hold: no call happens)
    R2->>M: useState(false) → reads cell 1 = '' ❌
    Note over R2: `confirmed` ends up as ''<br/>(a string) instead of false

The result on screen: confirmed ends up holding '' instead of false, and setConfirmed writes into the cell that used to belong to note. There's no syntax error, the app doesn't crash, and the bug shows up as absurd behaviour that's very hard to trace. React catches many of these cases and warns with:

Warning: React has detected a change in the order of Hooks called by BookingPanel.
This will lead to bugs and errors if not fixed.

When you see that message, look for a hook inside an if, a loop, a try/catch, or after an early return. It's always that.

The practical consequence: the number and order of hook calls must be identical on every render of a component. That's why hooks are all written together, right at the top, before any conditional logic. And if you need a conditional value, the condition goes inside the hook, not around it:

// Instead of putting the hook inside the if, put the if inside the hook
const [note, setNote] = useState(bike ? '' : 'no bike');

  1. Rule 2: only from components or from other hooks

Call hooks only from React function components or from other custom hooks. Never from plain JavaScript functions.

// CORRECT: from a component
function FleetSummary({ fleet }) {
  const [order, setOrder] = useState('model');
  // …
}

// CORRECT: from a custom hook (name starts with use)
function useSortedFleet(fleet) {
  const [order, setOrder] = useState('model');
  return { order, setOrder };
}

// INCORRECT: from a plain function
function calculateAvailable(fleet) {
  const [counter, setCounter] = useState(0);   // ❌ which component does this state belong to?
  return fleet.filter((b) => b.status === 'disponible').length;
}

// INCORRECT: from an event handler
function handleClick() {
  const [open, setOpen] = useState(false);  // ❌ runs outside of render
}

The reason follows straight from the previous section: the cells belong to a component instance that's rendering right now. If you call a hook from a loose function, React has no idea which cell list to reach for, and it throws:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

The event handler case deserves a warning, because it's a frequent mistake for beginners: handlers run after render, once no rendering is in progress anymore. Hooks get called during render; handlers use whatever those hooks returned.

// The correct pattern
function BikeCard({ bike }) {
  const [featured, setFeatured] = useState(false);   // hook: during render

  function handleClick() {
    setFeatured(!featured);                          // handler: uses the result
  }

  return <article onClick={handleClick}>…</article>;
}

And the constructive consequence of this rule is the one that gives everything meaning: a function whose name starts with use and calls hooks is a custom hook, and that's all it takes to reuse stateful logic. No wrappers, no new levels in the tree. You'll build one in Custom Hooks.

  1. eslint-plugin-react-hooks, the safety net

The two rules are easy to break without noticing, especially while refactoring. That's why the React team maintains an ESLint plugin that checks them automatically, and that Vite's React templates already ship configured.

npm install --save-dev eslint-plugin-react-hooks
// eslint.config.js (excerpt)
import reactHooks from 'eslint-plugin-react-hooks';

export default [
  {
    plugins: { 'react-hooks': reactHooks },
    rules: {
      'react-hooks/rules-of-hooks': 'error',        // the two rules from this lesson
      'react-hooks/exhaustive-deps': 'warn'         // effect dependencies (05-02)
    }
  }
];

What each rule contributes:

Rule What it catches Recommended severity
rules-of-hooks Hooks in conditionals, loops, plain functions, or handlers error: there are no false positives that would justify ignoring it
exhaustive-deps Missing or extra dependencies in useEffect, useMemo, and useCallback warn: it's almost always right, but there are legitimate exceptions

The first rule is wrong so rarely that it's worth treating as a compile error. If you're ever tempted to silence it with a comment, the correct answer is almost always to restructure the component — extract a child component, move the condition inside the hook — not to silence the warning.

  1. Catalogue of the course's hooks

Here's the full map. Don't memorize the details now: the right-hand column tells you where each one gets explained in depth.

Hook What it's for, in one sentence Where it's studied
useState Store a value that changes and triggers a new render when it changes 05-01
useEffect Synchronize the component with an external system (timers, network, subscriptions) 05-02
useRef Store a value without triggering renders, and access DOM nodes 05-03
useContext Read a value shared by an entire subtree, without prop drilling 05-04
useReducer Manage complex state with many transitions, via actions and a reducer 05-05
useMemo Remember the result of an expensive calculation across renders 08-03
useCallback Remember a function across renders so it doesn't break the children's memoization 08-03
useId Generate a unique, stable identifier to associate a label with fields 05-03 (mention) and accessibility from 03-06
useTransition Mark an update as non-urgent so it doesn't block the interface Module 8
useDeferredValue Show a "lagging" version of a value while the final one arrives Module 8
use Read a promise or a context directly during render (React 19) Module 10
Custom hooks Package your own stateful logic into a reusable function 05-06

Two observations about the catalogue:

  • Most components only need useState. After that, by frequency, come useEffect and useContext. useMemo and useCallback are optimization tools that only apply once you've measured a problem, and that's exactly why they show up in module 8.
  • There are more hooks than the ones listed here (useImperativeHandle, useSyncExternalStore, useDebugValue, useOptimistic, useActionState…). They're specialized, and this course leaves them out except for the odd mention. The table above covers ninety-five percent of the React written on a daily basis.

  1. Combined basic use: SummaryPanel with useState + useEffect

A taste of module 5. This CicloUrbano component combines the two hooks you'll use most: it keeps a count of how many times the summary has been queried, and it synchronizes the browser tab's title with the available fleet.

// src/components/SummaryPanel.jsx
import { useState, useEffect } from 'react';
import styles from './SummaryPanel.module.css';

/**
 * Queryable summary of CicloUrbano's fleet.
 * Props:
 *  - fleet (array of Bike, required)
 */
function SummaryPanel({ fleet }) {
  const [queries, setQueries] = useState(0);
  const [message, setMessage] = useState('Click to refresh the summary.');

  // DERIVED values: recomputed on every render, they aren't state (04-01)
  const available = fleet.filter((bike) => bike.status === 'disponible').length;
  const inMaintenance = fleet.filter((bike) => bike.status === 'mantenimiento').length;

  // EFFECT: synchronizes the document title with the number available
  useEffect(() => {
    document.title = `CicloUrbano · ${available} bikes available`;
  }, [available]);

  function handleQuery() {
    setQueries((previousQueries) => previousQueries + 1);
    setMessage(
      available === 0
        ? 'There are no bikes available right now.'
        : `${available} of ${fleet.length} bikes are ready to rent.`
    );
  }

  return (
    <section className={styles.panel}>
      <h2>Fleet summary</h2>
      <p>Total: {fleet.length} · Available: {available} · In maintenance: {inMaintenance}</p>
      <p aria-live="polite">{message}</p>
      <p className={styles.counter}>Queries made: {queries}</p>
      <button type="button" onClick={handleQuery}>
        Refresh summary
      </button>
    </section>
  );
}

export default SummaryPanel;

What each piece does and why it's where it is:

  • The two useState calls sit at the top, at the top level, one after the other. They occupy cells 0 and 1, in that order, on every render. Rule 1 satisfied.
  • available and inMaintenance aren't state. They're computed from fleet on every render. If they were state, you'd have to recompute them by hand every time the fleet changed, with the desync risk you saw in 04-01.
  • setQueries((previousQueries) => …) uses the functional form because the new value depends on the previous one. It's the precaution from 02-04.
  • The useEffect synchronizes with an external system: document.title belongs to the browser, not to React. This is where the mental model from 04-03 applies: it's not about "running code on mount," it's about keeping the title synchronized with available. That's why available shows up in the dependency list [available]: whenever that number changes, the synchronization gets redone.
  • handleQuery doesn't call any hook. It uses the hooks' results (setQueries, setMessage, available), which is exactly the correct division of labour under rule 2.
  • The aria-live="polite" comes from 03-06: the message changes without the person's focus moving, and it needs to be announced.

And in App, with no ceremony at all:

// src/App.jsx (excerpt)
<SummaryPanel fleet={bikes} />

No HOC, no render props, no extra level in the tree. Two function calls inside the component, and it already has state and effects. That's the contribution of hooks, summed up in one example.

  1. Local state, ref, and context: three different things

To close out the map, the distinction that will organize most of the decisions you'll make in module 5:

Tool Stores a value that… Triggers a render when it changes Scope Covered in
State (useState) Shows up on screen and changes over time Yes The component instance 05-01
Ref (useRef) Needs remembering but isn't painted (a timer's identifier, a DOM node) No The component instance 05-03
Context (useContext) Many components far apart from each other need it (user, theme, language) Yes, in whoever consumes it The whole subtree under the provider 05-04

In one sentence each:

  • State: "what's on screen, and when it changes it needs repainting."
  • Ref: "what gets remembered between renders without anyone having to repaint." It's exactly the this.timer from ActivityPanel's class in 04-03.
  • Context: "what's available to a whole subtree without passing it hand to hand," the answer to the prop drilling left open in 04-01.

Common Mistakes and Tips

  • A hook inside an if, a loop, or a try. It breaks the correspondence by position and causes incomprehensible bugs. If you need conditionality, move the condition inside the hook or extract a child component.
  • A hook after an early return. It's the same mistake in disguise: the return makes the following lines not run on some renders. All hooks go before any return.
  • Calling a hook from an event handler. Handlers run outside of render. Call the hook above and use its result inside the handler.
  • Naming a function that calls hooks getSomething. Without the use prefix, the ESLint plugin doesn't recognize it as a hook and checks nothing inside it. The convention is functional, not cosmetic.
  • Believing that useState in a loop gives "one state per item." It doesn't: it gives a disaster. One state per item means one component per item.
  • Silencing rules-of-hooks with an ESLint comment. That's treating the symptom. The rule is practically never wrong.
  • Tip: write all the hooks together, at the start of the component. State, refs, context, and effects, in that order, then the derived logic, the handlers, and the return. Keep that layout across the whole project and following the rules becomes automatic.
  • Tip: look at hooks in React DevTools. Selecting a function component shows you the list of its hooks in order, with their values. It's the cell list from section 4, made visible.

Exercises

Exercise 1. This CicloUrbano component breaks the rules of hooks in three different places. Identify each violation, explain which rule it breaks and what concrete consequence it has, and rewrite the component correctly.

function StationPanel({ station, showDetail }) {
  const [freeDocks, setFreeDocks] = useState(station.docks);

  if (!station) {
    return <p>Station not found.</p>;
  }

  if (showDetail) {
    const [detailOpen, setDetailOpen] = useState(true);
  }

  function handleRental() {
    const [lastRental, setLastRental] = useState(null);
    setFreeDocks(freeDocks + 1);
  }

  return (
    <article>
      <h3>{station.name}</h3>
      <p>Free docks: {freeDocks}</p>
      <button type="button" onClick={handleRental}>Return bike</button>
    </article>
  );
}

Exercise 2. Without writing code, say which hook you'd use for each CicloUrbano situation and which lesson covers it. Justify each choice briefly.

  1. Remember which bike type is filtered in the catalogue.
  2. Store a setTimeout's identifier so it can be cancelled.
  3. Have fifteen components scattered across the tree know the signed-in user.
  4. Keep document.title up to date with the number of active bookings.
  5. Manage a booking form with eight fields and complex transitions between validation states.

Exercise 3. Extend section 8's SummaryPanel with a third piece of state, lastQuery, that stores the time of the last query as a readable string (toLocaleTimeString('en-GB')), or null if it hasn't been queried yet. Show it on screen only when it exists. Then answer: which position in the cell list does this new state land on, and why does it matter where you declare it?

Solutions

Solution 1. The three violations:

  1. Early return before a hook (rule 1). When station is null, the function ends before reaching the following calls, and the number of hooks executed changes between renders. What's more, useState(station.docks) on the line above would already have blown up trying to read .docks off null: the guard arrives too late.
  2. useState inside an if (rule 1). detailOpen only gets created when showDetail is true; when that prop changes, the cells shift and the states get mixed up with each other.
  3. useState inside an event handler (rule 2). handleRental runs after render, when there's no render in progress: React throws "Invalid hook call."

And a fourth problem, not about the rules but about correctness: setFreeDocks(freeDocks + 1) should use the functional form, and it also has no upper bound (it could exceed the station's total docks).

function StationPanel({ station, showDetail }) {
  // 1. ALL hooks at the top, unconditional and before any return
  const [freeDocks, setFreeDocks] = useState(station ? station.docks : 0);
  const [detailOpen, setDetailOpen] = useState(true);
  const [lastRental, setLastRental] = useState(null);

  // 2. Early returns, AFTER the hooks
  if (!station) {
    return <p>Station not found.</p>;
  }

  // 3. The handler uses the hooks' results; it doesn't call any
  function handleRental() {
    setFreeDocks((previous) => Math.min(station.docks, previous + 1));
    setLastRental(new Date().toLocaleTimeString('en-GB'));
  }

  return (
    <article>
      <h3>{station.name}</h3>
      <p>Free docks: {freeDocks}</p>
      {showDetail && detailOpen && <p>District: {station.district}</p>}
      {lastRental && <p>Last return: {lastRental}</p>}
      <button type="button" onClick={handleRental}>Return bike</button>
    </article>
  );
}

Notice the general pattern behind the fix: hooks are always all declared; what's conditional is the use of their value in the JSX, not the call itself.

Solution 2.

Case Hook Lesson Why
1. Filtered type in the catalogue useState 05-01 It's a visible value that changes with interaction and must trigger a new render. It's lived in App since 04-01
2. A setTimeout identifier useRef 05-03 It needs to be remembered across renders but isn't painted; changing it shouldn't repaint anything. It's the this.timer from 04-03
3. Session user, in fifteen components useContext 05-04 Lifting it to App and passing it through props would cause the prop drilling flagged in 04-01
4. document.title up to date useEffect 05-02 The document title is a system external to React; it needs to be synchronized with a value from the component
5. Eight-field form with transitions useReducer 05-05 With many fields and transition rules, a reducer centralizes the logic better than eight loose useState calls

Solution 3.

function SummaryPanel({ fleet }) {
  const [queries, setQueries] = useState(0);                 // cell 0
  const [message, setMessage] = useState('Click to refresh the summary.');  // cell 1
  const [lastQuery, setLastQuery] = useState(null);          // cell 2

  const available = fleet.filter((bike) => bike.status === 'disponible').length;
  const inMaintenance = fleet.filter((bike) => bike.status === 'mantenimiento').length;

  useEffect(() => {
    document.title = `CicloUrbano · ${available} bikes available`;
  }, [available]);

  function handleQuery() {
    setQueries((previousQueries) => previousQueries + 1);
    setLastQuery(new Date().toLocaleTimeString('en-GB'));
    setMessage(
      available === 0
        ? 'There are no bikes available right now.'
        : `${available} of ${fleet.length} bikes are ready to rent.`
    );
  }

  return (
    <section className={styles.panel}>
      <h2>Fleet summary</h2>
      <p>Total: {fleet.length} · Available: {available} · In maintenance: {inMaintenance}</p>
      <p aria-live="polite">{message}</p>
      <p className={styles.counter}>Queries made: {queries}</p>
      {lastQuery && <p>Last query: {lastQuery}</p>}
      <button type="button" onClick={handleQuery}>Refresh summary</button>
    </section>
  );
}

lastQuery occupies cell 2, because it's the third hook called. What matters isn't the number itself, but that that position stays the same across every render: that's why the declaration sits at the top level, next to the other two, and not inside the if that decides whether to show it. The condition applies only to the JSX ({lastQuery && …}), never to the hook call. If you had declared it inside a conditional, on the renders where that condition didn't hold, useEffect would end up reading the wrong cell.

Conclusion

A hook is a function whose name starts with use and that hooks a function component into React's internal system, giving it access to state, effects, context, and everything that used to require a class. Hooks were born to solve three concrete problems classes carried with them: the impossibility of giving state to a function, the wrapper hell produced by HOCs and render props (04-02), and the logic of a single concern scattered across lifecycle methods (04-03).

Their two rules — only at the top level and only from components or from other hooks — aren't stylistic whims: they're explained by the mechanism of the ordered cell list React keeps per instance. The link between a useState call and its stored value is position, and that's why the number and order of the calls must be identical on every render. eslint-plugin-react-hooks watches both rules for you, and it's worth treating rules-of-hooks as an error. You also now have the full map: which hooks exist, what each one is for, and which lesson covers it; plus a first SummaryPanel from CicloUrbano that combines useState with useEffect without a single wrapper.

One piece remains to close out the module, and it's the exception that proves everything above. Hooks have replaced lifecycle methods in everything… except one case: catching errors a component throws during render. If a CicloUrbano card blows up on unexpected data, React unmounts the whole tree and the person is left staring at a blank page. Avoiding that requires a tool that, to this day, still requires a class component. The next lesson is Error Boundaries: Catching Failures in the UI.

React Course

Module 1: Getting Started with React

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved