Skip to content
DevDepth
← Back to all articles

In-Depth Article

Why Hooks Can’t Be Used in Conditions, Loops, or Nested Functions

React Hooks rely on call order, not variable names. Learn how Fiber stores hooks in a linked list and why changing the call order breaks state alignment.

Published: Updated: 8 min readreact-internals

When people first learn React Hooks, they usually remember one rule:

Hooks must be called at the top level of a function component. You cannot put them inside if statements, loops, or regular nested functions.

At first glance, this can feel like an arbitrary rule. But it is not just a stylistic restriction. The real reason is that React depends entirely on the order in which Hooks are called to keep state associated with the right Hook.

Put more simply:

React does not identify a Hook by your variable name. It identifies a Hook by its position in the call order during render.

That is why putting a Hook inside a condition, loop, or nested function can break the call order and lead to state mismatches, corrupted data, or runtime errors.

Let’s break this down from two angles: the underlying data structure and the runtime behavior.

1. React does not manage Hooks by name. It manages them by order.

Take this simple example:

function MyComponent() {
  const [name, setName] = useState("Alice");
  useEffect(() => {});
  const [age, setAge] = useState(25);
}

As developers, we think of this as:

first Hook: useState("Alice")

second Hook: useEffect(...)

third Hook: useState(25)

But React does not really care that the variables are named name and age. What it cares about is:

what was the first Hook call

what was the second Hook call

what was the third Hook call

So the key is not the variable name. The key is the order of calls.

2. Under the hood, Hooks are stored as a linked list on the Fiber

In React’s Fiber architecture, each function component has a corresponding Fiber node. That Fiber stores various pieces of information related to the component, including its Hooks.

For Hooks, React keeps them on the Fiber’s memoizedState. The important detail here is this:

memoizedState is not a map keyed by name. It is not an object where React stores name and age. It is a linked list built in call order.

Using the same example:

function MyComponent() {
  const [name, setName] = useState("Alice"); // Hook 1
  useEffect(() => {});                       // Hook 2
  const [age, setAge] = useState(25);        // Hook 3
}

You can roughly imagine the internal structure like this:

Fiber.memoizedState
   ↓
[Hook1: useState -> state: "Alice"]
   ↓
[Hook2: useEffect -> effect info]
   ↓
[Hook3: useState -> state: 25]
   ↓
 null

Each node in this list represents one Hook.

React only knows that:

the first node belongs to the first Hook call

the second node belongs to the second Hook call

the third node belongs to the third Hook call

It does not know that one is “name” and another is “age.” It only matches them by position.

3. During render, React walks through Hooks one by one in order

Once the linked list idea is clear, the next step is understanding how React reads it during rendering.

Initial render

The first time a component renders, React creates a new Hook node every time it sees a Hook call, and appends it to the list.

So the first render is basically building the list.

Re-render

On later renders, React does not randomly match Hooks again. Instead, it starts from the head of the Hook list and reads each Hook node in order.

You can think of React as maintaining an internal pointer to “the current Hook being processed.”

So when the component renders again:

function MyComponent() {
  const [name, setName] = useState("Alice");
  useEffect(() => {});
  const [age, setAge] = useState(25);
}

React roughly does this:

reset the pointer to the head of the Hook list

execute the first Hook call and read the first node

move the pointer forward

execute the second Hook call and read the second node

move the pointer forward

execute the third Hook call and read the third node

This only works if one assumption stays true:

On every render, Hooks must be called in exactly the same order as before.

That is what allows React to guarantee that the first Hook call always maps to the first node, the second Hook call always maps to the second node, and so on.

4. Why Hooks cannot go inside conditions

Now let’s look at the main problem.

Suppose you put a Hook inside a condition:

function MyComponent({ someCondition }) {
  const [name, setName] = useState("Alice");

  if (someCondition) {
    useEffect(() => {});
  }

  const [age, setAge] = useState(25);
}

On the first render, if someCondition === true, the call order is:

useState(name)

useEffect

useState(age)

The Hook list becomes:

[Hook1: name] -> [Hook2: effect] -> [Hook3: age]

But on the next render, if someCondition === false, the execution order becomes:

useState(name)

skip useEffect

useState(age)

This is where things break.

React still tries to read the Hook list in the old order:

first Hook call reads the first node, fine

then React expects the second Hook call to match the second node

But in this render, the second Hook call is now useState(age), while the second node in the old list is still the useEffect node.

So now the positions no longer line up.

The place that expects to read the age state is now reading the old effect node instead. From that point on, every Hook after it becomes misaligned.

That is why React throws errors like:

Rendered fewer hooks than expected

Rendered more hooks than expected

The underlying issue is always the same:

The number or order of Hook calls changed between renders.

5. Why Hooks cannot go inside loops

The problem with loops is basically the same as with conditions: the number of Hook calls can become unstable.

For example:

function MyComponent() {
  for (let i = 0; i < 3; i++) {
    useState(i);
  }
}

If that loop always ran exactly three times forever, it might look safe. But React does not allow this pattern, because loops naturally imply that the number of iterations could change.

Maybe it runs three times on one render, then two times on the next render. If that happens, the Hook order breaks in exactly the same way.

So React uses a simple rule:

Hooks cannot be used inside loops because loops make the number of Hook calls unstable.

6. Why Hooks cannot go inside nested functions

Now consider this:

function MyComponent() {
  function handleSomething() {
    useState(123);
  }

  handleSomething();

  return null;
}

Or this:

function MyComponent() {
  const doWork = () => {
    useEffect(() => {});
  };

  doWork();

  return null;
}

The problem here is that React cannot guarantee that the nested function will be called on every render, in the same place, the same number of times.

Even if it happens to work in one version of the code, it becomes fragile immediately if later:

the function is no longer called

it gets called more than once

it only gets called inside a branch

it gets moved into an event handler or callback

At that point, Hook order breaks again.

That is why React requires this:

Hooks must be called directly at the top level of a function component, or at the top level of a custom Hook. They cannot be hidden inside regular functions.

7. Why custom Hooks are allowed

A common question is: if Hook order matters so much, why are custom Hooks allowed?

For example:

function useUserInfo() {
  const [name, setName] = useState("Alice");
  useEffect(() => {});
  return { name };
}

function MyComponent() {
  const user = useUserInfo();
  return <div>{user.name}</div>;
}

The answer is simple:

A custom Hook is still just a fixed sequence of Hook calls that runs during rendering.

React does not treat useUserInfo() as some mysterious black box. What matters is that the Hooks inside it also run in a stable order every time.

As long as the custom Hook itself follows the same rules:

no Hooks inside conditions

no Hooks inside loops

no Hooks inside regular nested functions

then it works fine as part of the overall Hook order.

So a custom Hook is really just a way to package a stable sequence of Hook calls for reuse. It does not change the underlying rule.

8. The real rule is not just “don’t do this.” The real rule is “keep call order stable.”

A lot of explanations stop at surface-level rules like:

do not put Hooks inside if

do not put Hooks inside for

do not put Hooks inside nested functions

Those rules are correct, but they are only the surface.

The deeper rule is this:

Hooks only work because React assumes they will be called in exactly the same order on every render.

So any pattern that can cause one of the following is unsafe:

a Hook is called on one render but skipped on the next

a Hook is called fewer times than before

a Hook is called more times than before

the relative order of Hook calls changes

Once that happens, React can no longer correctly match the Hook calls in your code with the Hook nodes stored on the Fiber.

9. Why doesn’t React just give each Hook a key?

You might wonder:

If relying on order is so fragile, why doesn’t React assign each Hook a unique key, something like this?

useState("name", "Alice"); useState("age", 25);

At first glance, that sounds more reliable.

But React intentionally does not work that way, mainly because of a few trade-offs.

First, simplicity. If every Hook needed a manual key, Hook code would become much more verbose, and developers would have to deal with new problems like key collisions and key maintenance.

Second, composition. One of the biggest strengths of Hooks is that custom Hooks are easy to compose. Requiring identifiers for every internal Hook would make composition much clumsier.

Third, a straightforward runtime model. Reading a linked list in call order is a direct and efficient implementation model. React does not need to maintain an additional name-based lookup system.

So React made a deliberate trade-off:

Instead of asking developers to name every Hook, React asks developers to keep the Hook call order stable.

That is the design choice.

10. How to write this correctly in real code

A simple rule of thumb is:

Declare the Hook at the top level first, then put the condition inside the Hook.

So instead of writing this:

if (visible) {
  useEffect(() => {
    console.log("visible");
  }, []);
}

write this:

useEffect(() => {
  if (visible) {
    console.log("visible");
  }
}, [visible]);

The difference is crucial:

in the first version, the condition decides whether the Hook exists at all

in the second version, the Hook always exists, and only the logic inside it is conditional

React allows the second pattern, because the Hook call order remains stable.

11. Summary

Hooks cannot be used inside conditions, loops, or nested functions not because React wants to impose a random syntax rule, but because of how Hooks are implemented internally.

Inside React:

Hooks are stored on the Fiber as a linked list in call order

React reads them in the same order on every render

it does not match Hooks by variable name, but by position in the call sequence

So once the order changes, React starts reading the wrong Hook nodes, which causes state mismatches, broken logic, and runtime errors.

You can reduce the whole idea to one sentence:

Hooks cannot be used inside conditions, loops, or nested functions because React relies on a stable call order to match each Hook call with the correct state node. Once that order changes, the entire Hook system loses alignment.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-16

Contact the editor