The rules of hooks
Learn the rules of hooks: call them at the top level of components and hooks only, see why React needs the same order, and let ESLint check it.
- 01Follow the two rulesCall hooks only at the top level of a component or custom hook, before any early return, and never in conditions, loops, handlers or try blocks.
- 02Explain why order mattersDescribe how React matches each hook call to its saved state by position, and read the warning and errors it gives when the order changes.
- 03Use the toolsTell hooks by their
usenames, callusein a condition when you need to, and leteslint-plugin-react-hookscatch mistakes as you type.
An order card that breaks when you place an order
Hooks are the functions whose names start with use: useState, useEffect, useRef and the rest. They come with two rules. Here is a card that breaks one of them. Before there is an order, it shows a short message; after, it shows the order with a star rating. It looks fine. Click Place an order.
import { useState } from "react"; function OrderStatus({ order }) { const [showDetails, setShowDetails] = useState(false); if (order === null) { return <p>No order yet.</p>; } const [stars, setStars] = useState(0); return ( <div> <p>Order {order.id}: {order.item}</p> <button onClick={() => setShowDetails(!showDetails)}>Details</button> {showDetails && <p>Arrives on Friday.</p>} <button onClick={() => setStars(stars + 1)}>Stars: {stars}</button> </div> );} export default function App() { const [order, setOrder] = useState(null); return ( <div> <button onClick={() => setOrder({ id: 42, item: "Green tea" })}>Place an order</button> <OrderStatus order={order} /> </div> );}The card disappears, and the result shows React's error instead: Rendered more hooks than during the previous render. Line 4 calls useState for the details. While there is no order, line 6 returns early, so the render never reaches the useState on line 9. After the click, it does. The first render called one hook and the second called two, and React can't cope with that. Before the error, the console also shows a warning from React, with a table of the hooks:
React has detected a change in the order of Hooks called by OrderStatus. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useState useState
2. undefined useState
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Read the table row by row. In the previous render, hook 1 was a useState and there was no hook 2 (undefined). In the next render, hook 2 is a useState. React numbers the hooks, because the order they are called in is the only way it can tell them apart.
OrderStatus stops at the early return, so React saves one hook. Once there is an order, the render reaches a second useState, and React has nothing saved for it.The fix is to move the hook above the early return, so every render calls both:
import { useState } from "react"; function OrderStatus({ order }) { const [showDetails, setShowDetails] = useState(false); const [stars, setStars] = useState(0); if (order === null) { return <p>No order yet.</p>; } return ( <div> <p>Order {order.id}: {order.item}</p> <button onClick={() => setShowDetails(!showDetails)}>Details</button> {showDetails && <p>Arrives on Friday.</p>} <button onClick={() => setStars(stars + 1)}>Stars: {stars}</button> </div> );} export default function App() { const [order, setOrder] = useState(null); return ( <div> <button onClick={() => setOrder({ id: 42, item: "Green tea" })}>Place an order</button> <OrderStatus order={order} /> </div> );}Lines 4 and 5 now run on every render, order or no order, and only then does line 7 decide what to show. Click Place an order, then Stars and Details: each keeps its own value.
A small gym class has no names on its lockers. The first person through the door each week takes locker 1, the second takes locker 2, and each finds their own things, because they always arrive in the same order. If one person stays home, the person behind them opens locker 1 and finds someone else's bag.
- In real life: The gym's row of lockers for one class
- In React: React's list of hooks for one copy of a component
- In real life: The first person in gets locker 1, the second locker 2
- In React: The first hook call gets saved value 1, the second value 2
- In real life: Everyone arrives in the same order every week
- In React: Every render calls the same hooks in the same order
- In real life: One person skips a week, and the next one opens the wrong locker
- In React: A hook inside an
if: the next hook gets another hook's value
The rules of hooks: call hooks only at the top level of a component or a custom hook, before any early return, never inside conditions, loops, nested functions or try blocks; and call them only from components and custom hooks. React finds each hook's saved value by the order of the calls, so the order must be the same on every render.
Call hooks at the top level
The top level of a component is its own body: the lines that run every time React calls it, before any return. A hook written there runs on every render, in the same place. Anywhere else, it might run on some renders and not others, or a different number of times. This component, which doesn't run, puts a useState in each place a hook can't go:
function SearchBox({ open }) {
if (open) {
const [query, setQuery] = useState(""); // In a condition
}
for (const tab of ["Tea", "Cake"]) {
const [count, setCount] = useState(0); // In a loop
}
function handleClick() {
const [clicked, setClicked] = useState(false); // In an event handler
}
const total = useMemo(() => {
const [discount] = useState(0); // In a function passed to another hook
return 4 - discount;
}, []);
try {
const [saved, setSaved] = useState(false); // In a try block
} catch {
// Nothing here.
}
if (!open) return null;
const [page, setPage] = useState(1); // After an early return
}Line 3 runs only when open is true, and line 7 runs once per tab. Line 11 runs only when someone clicks, long after the render. Line 15 runs whenever useMemo decides to call its function. Line 20 is inside a try block, and line 26 comes after an early return, so it only runs while open is true.
You never need a hook in those places, because the thing you wanted to make conditional can go somewhere else. The most common fix is to call the hook every time and put the condition inside it. This cart saves itself, but not while it's empty. Click Add to cart twice.
import { useEffect, useState } from "react"; export default function App() { const [items, setItems] = useState(0); useEffect(() => { if (items === 0) return; console.log("Saved the cart, items:", items); }, [items]); return <button onClick={() => setItems(items + 1)}>Add to cart ({items})</button>;}The Effect on lines 6 to 9 is called on every render, so React always finds it in the same place. Line 7 is the condition: an ordinary if inside the Effect's function, which returns early while items is 0. The console stays empty at first, then logs Saved the cart, items: 1 and Saved the cart, items: 2.
For a loop, give each item a component of its own. A useState inside map() would make the number of hooks follow the number of items:
<ul>
{items.map((name) => {
const [quantity, setQuantity] = useState(1); // Not allowed: a hook in a loop
return <li key={name}>{name} × {quantity}</li>;
})}
</ul>Here each cart line is a CartLine component instead, with its own useState at its top level:
import { useState } from "react"; function CartLine({ name }) { const [quantity, setQuantity] = useState(1); return ( <li> {name} × {quantity} <button onClick={() => setQuantity(quantity + 1)}>Add one {name}</button> </li> );} export default function App() { const items = ["Green tea", "Carrot cake", "Lemonade"]; return ( <ul> {items.map((name) => <CartLine key={name} name={name} />)} </ul> );}Line 4 is the top level of CartLine, and line 18 renders one CartLine per item, each with a key. Every copy has its own list of hooks with one hook in it, so the list never changes length. Click Add one Carrot cake: only that line goes up. Each row of this table is a place you might be tempted to call a hook, and what to do instead.
| Where the hook is | Allowed? | What to do instead |
|---|---|---|
At the top of a component or custom hook, before any return | Yes. | This is where every hook goes. |
Inside an if | No. | Call the hook every time, and put the if inside it, like inside the Effect's function. |
After an early return | No. | Move the hook above the return. |
Inside a loop or map() | No. | Make a component for one item, and call the hook in that component. |
| Inside an event handler | No. | Call the hook at the top, and use what it gives you, like a set function, in the handler. |
Inside a function passed to useMemo, useEffect or another hook | No. | Call it at the top level, next to that hook. |
Inside a try block | No. | Call it outside the try. Errors from rendering are caught with an error boundary instead. |
Call hooks only from components and hooks
React keeps a list of hooks only while it is rendering a function component. A hook called at any other time has no list to go into. An event handler is the usual way this happens. Click Add a cup of tea and read the console.
import { useState } from "react"; export default function App() { function handleAdd() { const [cups, setCups] = useState(0); setCups(cups + 1); } return <button onClick={handleAdd}>Add a cup of tea</button>;}handleAdd runs on line 9's click, after the render has finished. Line 5 calls useState then, with no render to answer it, and React throws an error that starts Invalid hook call. Hooks can only be called inside of the body of a function component. and lists possible reasons; the second is "You might be breaking the Rules of Hooks". The result shows that error in place of the button. The fix keeps the state at the top, and the handler only uses its set function:
import { useState } from "react"; export default function App() { const [cups, setCups] = useState(0); function handleAdd() { setCups(cups + 1); } return <button onClick={handleAdd}>Add a cup of tea ({cups})</button>;}Line 4 calls useState while rendering. Line 7 calls setCups, which is a plain function, not a hook, so the handler may call it whenever it likes. Click the button: the count goes up.
Which functions count as components and hooks? React and its tools go by the name. A component is a function whose name starts with a capital letter, like ProductCard, that React calls when you write <ProductCard />. A hook is a function whose name is use followed by a capital letter or a number, like useState or useCart. You can write hooks of your own, called custom hooks, and a custom hook may call other hooks at its own top level; the next lesson, Reusing logic with custom hooks, is about them. Name a function use… only if it calls hooks: the name tells people, and the linter, that its calls follow these rules.
How to read the table: each row is a function, what React sees in it, and whether it may call hooks.
| The function | What it is | May it call hooks? |
|---|---|---|
function ProductCard() { … }, used as <ProductCard /> | A component: its name starts with a capital letter, and React calls it. | Yes. |
function useCart() { … } | A custom hook: its name is use and then a capital letter. | Yes. |
function formatPrice(amount) { … } | A plain function. | No. Call the hook in the component, and pass the value in. |
function handleClick() { … }, inside a component | An event handler. It runs after a click, not while React renders. | No. Use what the hook gave you at the top. |
class Cart extends Component { … } | A class component. | No. Hooks only work in function components. |
function userName() { … } | A plain function: after use comes a small letter. | No. |
Why the order of calls matters
In State: a component's memory you saw that useState doesn't say which state it wants, and React doesn't read your variable names. For each copy of each component, React keeps a list, and the first hook call on every render gets the first saved value. Here is that idea as a small model in plain JavaScript, written for this lesson. It isn't React's code, but it does the same two checks React does. Press Edit & run to see it print.
// A small model of React's list of hooks for one component.
let saved = []; // the hooks from the last render, in order
let calls = []; // the hooks called in this render, in order
function useHook(kind, initial) {
const index = calls.length;
const old = saved[index];
if (saved.length > 0 && old === undefined) {
throw new Error("Rendered more hooks than during the previous render.");
}
if (old && old.kind !== kind) {
console.log(`Warning: hook ${index + 1} was ${old.kind}, now ${kind}`);
}
calls.push({ kind, value: old ? old.value : initial });
return calls[index].value;
}
function render(component, props) {
calls = [];
const shown = component(props);
if (calls.length < saved.length) {
throw new Error("Rendered fewer hooks than expected.");
}
saved = calls;
return shown;
}
function DeliveryField({ pickup }) {
if (pickup) {
const store = useHook("useState", "Pune store");
return `Store: ${store}`;
}
const address = useHook("useState", "12 Park Street");
return `Address: ${address}`;
}
console.log(render(DeliveryField, { pickup: false }));
console.log(render(DeliveryField, { pickup: true }));Lines 2 and 3 hold the hooks saved from the last render and the hooks called in this one. useHook (lines 5 to 16) takes the next place in the list, line 6. Line 8 is React's first check: after the first render, a call with nothing saved at its place stops the render. Lines 14 and 15 store and return the saved value, or the starting value on the first render. render (lines 18 to 26) starts an empty list, calls the component, and does React's second check on line 21: fewer calls than last time also stops the render.
DeliveryField on lines 28 to 35 calls one hook in each branch. Line 37 renders it for delivery and prints Address: 12 Park Street. Line 38 renders it for pickup, and prints Store: 12 Park Street. Neither check fires: both renders made one useState call, so the store simply took the address from place 1. Now try real React: tick Pick up in store.
import { useState } from "react"; function DeliveryField({ pickup }) { if (pickup) { const [store] = useState("Pune store"); return <p>Store: {store}</p>; } const [address] = useState("12 Park Street"); return <p>Address: {address}</p>;} export default function App() { const [pickup, setPickup] = useState(false); return ( <div> <label> <input type="checkbox" checked={pickup} onChange={(e) => setPickup(e.target.checked)} /> Pick up in store </label> <DeliveryField pickup={pickup} /> </div> );}The page says Store: 12 Park Street, and the console stays empty: no warning, no error. Line 5's useState("Pune store") is the first hook call of this render, so React hands it the value saved at place 1 by line 8 in the first render. That quiet mix-up is the worst kind of bug the rules prevent, because nothing points at it.
store is the first useState call, so it gets that value: "12 Park Street". Nothing tells React the call is for a different variable.In the model, press Edit & run and add useHook("useEffect"); on a new line after line 30. Now the pickup render calls two hooks after a render that saved one, and line 9 throws Rendered more hooks than during the previous render., the overview's error. Real React stops the render at line 1048 of ReactFiberHooks.js, with the same words.
When the kinds of hook differ too, React notices more. Pick the first and the next render below, and read what React finds at each place in GiftForm's list.
function GiftForm({ gift }) { const [name, setName] = useState("Asha"); if (gift) { const [message, setMessage] = useState("Happy birthday!"); } const [city, setCity] = useState("Pune"); useEffect(() => { document.title = `Order for ${name}`; }); return <p>{name}, {city}</p>;}- 1Saved:
useState: nameNow:useState: namename gets its own value, "Asha". - 2Saved:
useState: cityNow:useState: messagemessage gets city's saved value, "Pune". - 3Saved:
useEffect: the title EffectNow:useState: cityA useState where an Effect was saved: React warns, then stops, because an Effect keeps no state. - 4Saved:
nothingNow:useEffect: the title EffectNever reached: the render already stopped.
React warns that the order of hooks changed, then throws: Should have a queue. You are likely calling Hooks conditionally, which is not allowed. (https://react.dev/link/invalid-hook-call)
Gift off, then on: message gets the city's saved "Pune", and at place 3 city's useState meets the saved Effect. React warns, then throws Should have a queue. You are likely calling Hooks conditionally, which is not allowed. (https://react.dev/link/invalid-hook-call): an Effect keeps no state, so there is nothing for useState to read. Gift on, then off: city gets "Happy birthday!", React warns at place 3, and at the end it throws Rendered fewer hooks than expected. This may be caused by an accidental early return statement. Real React keeps this list as a chain of objects on each component's fiber; How hooks work inside reads that code.
use is the one you can call in a condition
React 19 added use, a function that reads a value from a context or a promise. Its name is the word use itself, and it is the one exception to the first rule: React's docs say it can be called inside conditions and loops. This price only reads the shop's currency once someone has signed in. Click Sign in.
import { createContext, use, useState } from "react"; const CurrencyContext = createContext("$"); function Price({ amount, visible }) { if (!visible) { return <p>Sign in to see prices.</p>; } const currency = use(CurrencyContext); return <p>Green tea: {currency}{amount}</p>;} export default function App() { const [signedIn, setSignedIn] = useState(false); return ( <CurrencyContext value="€"> <button onClick={() => setSignedIn(!signedIn)}>{signedIn ? "Sign out" : "Sign in"}</button> <Price amount={4} visible={signedIn} /> </CurrencyContext> );}Line 3 makes a context with "$" as its default, and line 17 provides "€". Price returns early on line 7 while you're signed out, and only then calls use(CurrencyContext) on line 9. The page shows Sign in to see prices., then Green tea: €4. Click Sign out and in again: no warning, no error.
Why is use allowed there? Because it doesn't take a place in the list of hooks. Here is the start of it, from ReactFiberHooks.js:
function use<T>(usable: Usable<T>): T {
// $FlowFixMe[invalid-compare]
if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding]
if (typeof usable.then === 'function') {
// This is a thenable.
const thenable: Thenable<T> = usable as any;
return useThenable(thenable);
} else if (usable.$$typeof === REACT_RECOVERABLE_TYPE) {
// Fiber is the final renderer, so there is no downstream host that
// needs to recover this subtree. Continue rendering through it.
return undefined as any;
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = usable as any;
return readContext(context);
}
}
// eslint-disable-next-line react-internal/safe-string-coercion
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}For a promise (line 5), it waits for the promise's value; for a context (line 13), it reads the nearest provider's value with readContext, line 15. Line 9 is for the value browser() returns, new in React 19.3, which in the browser reads as nothing. None of these paths saves anything in the component's list of hooks, so skipping a call doesn't move any other hook. The second rule still holds: use reads while rendering, so call it only from components and hooks, and not inside a try block. Reading a promise with use needs Suspense; The use API covers both.
Let the linter check the rules
You don't have to spot these mistakes yourself. A linter reads your code and warns about likely mistakes as you type, and React's own ESLint plugin, eslint-plugin-react-hooks, checks the rules of hooks. When this lesson was checked, in October 2026, its latest version was 7.1.1. With ESLint's flat config, you add the plugin's recommended preset in eslint.config.js:
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,jsx}"],
extends: [js.configs.recommended, reactHooks.configs.flat.recommended],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
]);Line 3 imports the plugin, and line 9 adds reactHooks.configs.flat.recommended to the settings for your .js and .jsx files; lines 10 to 13 tell ESLint about the browser and JSX. A Vite project with ESLint, from Setting up a React project, gets almost exactly this file. Vite's first choice of linter, Oxlint, turns on its own copy of the rule, react/rules-of-hooks, in .oxlintrc.json. Run npx eslint . on the order card from the overview, and ESLint reports:
src/App.jsx
9:29 error React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
✖ 1 problem (1 error, 0 warnings)9:29 is line 9, column 29: the useState after the early return. Your editor shows the same message under the line as you type, so the bug never reaches the page. Each row of the table is a wrong place from the six-places example and what the rule says about it.
eslint-plugin-react-hooks, your editor marks line 9 of the order card as you type, long before anyone clicks Place an order.| Where the hook is | The rule's message |
|---|---|
Inside an if | React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. |
After an early return | React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? |
| Inside a loop | React Hook "useState" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. |
Inside an event handler, handleClick | React Hook "useState" is called in function "handleClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". |
Inside the function passed to useMemo | React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. |
The rule doesn't mark a useState inside a try block, the one place in the list it misses, though React's docs still rule it out; it does mark use inside one. The preset has two classic rules: react-hooks/rules-of-hooks, an error, and react-hooks/exhaustive-deps, a warning, which checks dependency arrays as in Synchronizing with effects. Since version 7, recommended also turns on rules that come from the React Compiler, such as purity, refs, immutability, set-state-in-render and set-state-in-effect. They check React's other rules, like keeping components pure, and they work whether or not your app uses the Compiler. A preset called recommended-latest adds newer rules still being tried out.
When a rule complains, fix the code. A comment like // eslint-disable-next-line react-hooks/rules-of-hooks hides the message and keeps the bug.
A product page that follows the rules
A product page has all three temptations: a loading state that returns early, a page view to log only once the product has arrived, and reviews in a list, each with its own Helpful count. Click Load the product, then the buttons.
import { useEffect, useState } from "react"; const reviews = [ { id: 1, author: "Asha", text: "Lovely and fresh." }, { id: 2, author: "Ravi", text: "My favourite green tea." },]; function Review({ review }) { const [helpful, setHelpful] = useState(0); return ( <li> {review.author}: {review.text} <button onClick={() => setHelpful(helpful + 1)}>Helpful ({helpful})</button> </li> );} function ProductPage({ product }) { const [quantity, setQuantity] = useState(1); useEffect(() => { if (product === null) return; console.log("Viewed", product.name); }, [product]); if (product === null) { return <p>Loading the product…</p>; } return ( <div> <h3>{product.name}</h3> <button onClick={() => setQuantity(quantity + 1)}>Quantity: {quantity}</button> <ul> {reviews.map((review) => <Review key={review.id} review={review} />)} </ul> </div> );} export default function App() { const [product, setProduct] = useState(null); return ( <div> <button onClick={() => setProduct({ name: "Green tea" })}>Load the product</button> <ProductPage product={product} /> </div> );}Every hook in ProductPage comes first: useState on line 18 and the Effect on lines 20 to 23. The Effect checks for a product inside its function, on line 21, so the console logs Viewed Green tea only once it has loaded. Only then does line 25 return early while loading. Each review is a Review component (lines 8 to 15) with its own useState, and line 34 renders one per review. Click Helpful on Ravi's review: only his count goes up.
const [query, setQuery] = useState(""); // first line of SearchBoxif (open) { useEffect(() => { … }); }items.map((item) => { const [n, setN] = useState(0); … })useEffect(() => { if (open) { … } }, [open]);if (!user) return null; const [tab, setTab] = useState("info");if (showPrice) { const currency = use(CurrencyContext); }function handleClick() { const [n, setN] = useState(0); }function useCart() { const [items, setItems] = useState([]); return items; }
Sort each line: would React find this hook in the same place on every render, and is it called while rendering a component or hook?
Common mix-ups
- "A hook in an
ifis fine as long as it works when I test it." No. It works until the condition changes between renders. Then React throws, or, worse, hands a hook another hook's value with no warning at all. - "React matches hooks by their variable names." No. It never sees your names. The first call gets the first saved value, and so on.
- "An early
returnat the top is harmless." Only if every hook comes before it. A hook after an earlyreturnis a conditional hook. - "Set functions are hooks too, so I can't call them in handlers." No.
setCupsis a plain function thatuseStategave you. Call it anywhere, any time. - "Any function starting with
useis a hook." Onlyusefollowed by a capital letter or a number, likeuseCart, oruseitself.userNameis a plain function. - "
usemust follow every rule other hooks do." No. It can go in conditions and loops. It still has to be called while rendering a component or hook, and not insidetry.
Practice exercises
Each exercise shows its result under the code, or when you press Edit & run, with the console under it. Change the code, and the result runs again as you type. Make a guess first, then use the hints.
This menu item works today, because nothing changes soldOut. Which line breaks the rules of hooks?
import { useState } from "react";
function MenuItem({ name, soldOut }) {
const [liked, setLiked] = useState(false);
if (soldOut) {
return <p>{name} is sold out.</p>;
}
const [quantity, setQuantity] = useState(1);
return (
<p>
{name} × {quantity}{" "}
<button onClick={() => setQuantity(quantity + 1)}>Add one</button>
<button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>
</p>
);
}
export default function App() {
return <MenuItem name="Lemonade" soldOut={false} />;
}import { useState } from "react"; function MenuItem({ name, soldOut }) { const [liked, setLiked] = useState(false); if (soldOut) { return <p>{name} is sold out.</p>; } const [quantity, setQuantity] = useState(1); return ( <p> {name} × {quantity}{" "} <button onClick={() => setQuantity(quantity + 1)}>Add one</button> <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button> </p> );} export default function App() { return <MenuItem name="Lemonade" soldOut={false} />;}Line 8's useState(1) comes after the early return on lines 5 to 7, so it only runs when the item isn't sold out. Move it up next to line 4.
Click Subscribe. What error does React show in place of the result?
import { useEffect, useState } from "react"; export default function App() { const [subscribed, setSubscribed] = useState(false); useEffect(() => { console.log("Subscribed:", subscribed); }, [subscribed]); let note = ""; if (!subscribed) { const [email] = useState("asha@example.com"); note = `We'll write to ${email}.`; } return ( <div> <button onClick={() => setSubscribed(true)}>Subscribe</button> <p>{note}</p> </div> );}import { useEffect, useState } from "react"; export default function App() { const [subscribed, setSubscribed] = useState(false); useEffect(() => { console.log("Subscribed:", subscribed); }, [subscribed]); let note = ""; if (!subscribed) { const [email] = useState("asha@example.com"); note = `We'll write to ${email}.`; } return ( <div> <button onClick={() => setSubscribed(true)}>Subscribe</button> <p>{note}</p> </div> );}Before the click, the render calls useState, useEffect, useState. After it, the if skips line 12, so it calls two, and React throws Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
Fix the welcome card so signing in works. What does it say after you click Sign in?
import { useState } from "react"; function Welcome({ user }) { const [theme] = useState("light"); if (user === null) { return <p>Please sign in.</p>; } const [visits] = useState(1); return ( <p className={theme}> Welcome back, {user}. Visit number {visits}. </p> );} export default function App() { const [user, setUser] = useState(null); return ( <div> <button onClick={() => setUser("Asha")}>Sign in</button> <Welcome user={user} /> </div> );}import { useState } from "react"; function Welcome({ user }) { const [theme] = useState("light"); const [visits] = useState(1); if (user === null) { return <p>Please sign in.</p>; } return ( <p className={theme}> Welcome back, {user}. Visit number {visits}. </p> );} export default function App() { const [user, setUser] = useState(null); return ( <div> <button onClick={() => setUser("Asha")}>Sign in</button> <Welcome user={user} /> </div> );}With both hooks before the if, every render calls two hooks. After Sign in the card says Welcome back, Asha. Visit number 1.
The search should only log once there is something to search for, but typing breaks it. Fix it. After your fix, what does typing t log?
import { useEffect, useState } from "react"; export default function App() { const [query, setQuery] = useState(""); if (query !== "") { useEffect(() => { console.log("Searching for", query); }, [query]); } return <input placeholder="Search the menu" value={query} onChange={(e) => setQuery(e.target.value)} />;}import { useEffect, useState } from "react"; export default function App() { const [query, setQuery] = useState(""); useEffect(() => { if (query === "") return; console.log("Searching for", query); }, [query]); return <input placeholder="Search the menu" value={query} onChange={(e) => setQuery(e.target.value)} />;}The Effect is now called on every render; the check for an empty box moved inside it. Typing t logs Searching for t.
Adding lemonade breaks this cart. Fix it, then click Add lemonade and Add one Lemonade. What does the lemonade line show?
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Green tea", "Carrot cake"]); return ( <div> <ul> {items.map((name) => { const [quantity, setQuantity] = useState(1); return ( <li key={name}> {name} × {quantity} <button onClick={() => setQuantity(quantity + 1)}>Add one {name}</button> </li> ); })} </ul> <button onClick={() => setItems([...items, "Lemonade"])}>Add lemonade</button> </div> );}import { useState } from "react"; function CartLine({ name }) { const [quantity, setQuantity] = useState(1); return ( <li> {name} × {quantity} <button onClick={() => setQuantity(quantity + 1)}>Add one {name}</button> </li> );} export default function App() { const [items, setItems] = useState(["Green tea", "Carrot cake"]); return ( <div> <ul> {items.map((name) => <CartLine key={name} name={name} />)} </ul> <button onClick={() => setItems([...items, "Lemonade"])}>Add lemonade</button> </div> );}Each CartLine keeps one hook in its own list, however many lines there are. Add lemonade, then Add one Lemonade: the new line shows Lemonade × 2.
Ticking This is a gift breaks the form. Fix it so the message box can still appear only for gifts. Then tick the box and type Hi: what does the card line say?
import { useState } from "react"; export default function App() { const [gift, setGift] = useState(false); let messageBox = null; if (gift) { const [message, setMessage] = useState(""); messageBox = ( <div> <input placeholder="Gift message" value={message} onChange={(e) => setMessage(e.target.value)} /> <p>Card says: {message}</p> </div> ); } return ( <div> <label> <input type="checkbox" checked={gift} onChange={(e) => setGift(e.target.checked)} /> This is a gift </label> {messageBox} </div> );}import { useState } from "react"; function GiftMessage() { const [message, setMessage] = useState(""); return ( <div> <input placeholder="Gift message" value={message} onChange={(e) => setMessage(e.target.value)} /> <p>Card says: {message}</p> </div> );} export default function App() { const [gift, setGift] = useState(false); return ( <div> <label> <input type="checkbox" checked={gift} onChange={(e) => setGift(e.target.checked)} /> This is a gift </label> {gift && <GiftMessage />} </div> );}GiftMessage always calls its one hook. Ticking the box adds the whole component, with a list of its own, instead of adding a hook to App's list. Typing Hi shows Card says: Hi.
Check your understanding
Question 1 of 7Which of these calls breaks the rules of hooks?
Choose an answer to see the explanation.
Question 2 of 7You click Be formal. What does the page show?
Read the code, then predictimport { useState } from "react"; function Greeting({ formal }) { if (formal) { const [title] = useState("Dr. Rao"); return <p>Hello, {title}</p>; } const [nickname] = useState("Ravi"); return <p>Hi, {nickname}</p>; } export default function App() { const [formal, setFormal] = useState(false); return ( <div> <button onClick={() => setFormal(true)}>Be formal</button> <Greeting formal={formal} /> </div> ); }Choose an answer to see the explanation.
Question 3 of 7Why must a component call its hooks in the same order on every render?
Choose an answer to see the explanation.
Question 4 of 7Which name does React treat as a hook?
Choose an answer to see the explanation.
Question 5 of 7A render calls one more hook than the render before it. What does React do?
Choose an answer to see the explanation.
Question 6 of 7Can you call
useinside anif?Choose an answer to see the explanation.
Question 7 of 7In
eslint-plugin-react-hooks, which rule reportsReact Hook "useState" is called conditionally?Choose an answer to see the explanation.
Key takeaways
- Call hooks at the top level of a component or custom hook, before any early
return. - Never call them inside conditions, loops, event handlers, functions passed to other hooks, or
tryblocks. - Call hooks only from components and custom hooks: functions named with a capital letter, or
useand a capital letter. - React finds each hook's saved value by its place in the list. A changed order throws, warns, or quietly hands a hook the wrong value.
- Put the condition inside the hook, or give the conditional part a component of its own.
useis the exception: it can be called in a condition.eslint-plugin-react-hookschecks the rest as you type.
Same hooks, same order, every render.
Call them at the top level of components and hooks, and let the linter remind you.
Coming next: Reusing logic with custom hooks. You'll write hooks of your own, and they follow these same rules.