Understanding the Top-Level Rule of React Hooks: Insights from React Hooks Source Code and Data Structures
Introduction: The Rules of Hooks
When reviewing the Rules of Hooks in the React official documentation, you'll see an important reminder:
Only call Hooks at the top level
This short rule is highly relevant to the stability of your React app.
This states that Hooks can only be called at the top level, which may not be easy to understand at first glance. Looking further into this section, you'll find more detailed explanations and examples:
Don't call Hooks inside loops, conditions, nested functions, or try/catch/finally blocks. Instead, always use Hooks at the top level of your React function, before any early returns.
/** Simple example **/
function CounterGood() {
  // ✅ Good: top-level in a function component
  const [count, setCount] = useState(0);
  ......
}
function CounterBad() {
  const [isOn, setIsOn] = useState(false)
  // 🔴 Bad: inside a condition (to fix, move it outside!)
  if(isOn){
    const [count, setCount] = useState(0);
    ......
  }
  ......
}
From this, we learn that React Hooks cannot be used inside if/else conditions or other block scopes such as loops, nested functions, or try/catch, but only at the top level of a component or custom hook function.
The official documentation is quite clear and lists the scenarios where Hooks should not be used:
(Screenshot from React official documentation)
When developing React applications, developers typically use the official eslint-plugin-react-hooks linting rules, which automatically prevent developers from writing code that breaks the Hooks rules.
If a developer accidentally places Hooks in an if/else condition, they'll see a warning like "React Hook "useXXX" is called conditionally. React Hooks must be called in the exact same order in every component render".
But why is this the case?
Why must Hooks be restricted to the top level and not be called in conditions, loops, or similar contexts?
This must be related to how Hooks are implemented, so we'll dive into the React source code. The following sections will cover:
- Finding the data structure of Hooks in the React source code
 - Understanding the data structure of Hooks during execution by implementing a simple 
useState - What problems arise when breaking the "Hooks must be called at the top level" rule
- What happens if we add conditions when using useState?
 - What happens if we add loops when using useState?
 
 - Conclusion: Remember to call Hooks at the top level
 
Let's continue with curiosity about this question!
Finding the Implementation and Data Structure of Hooks in React Source Code
Since React is open source, when we have questions about the logic behind Hooks, we can go directly to the official GitHub repo to find the actual code.
The core code for React Hooks is mainly located in the ReactFiberHooks.js related files. This section will focus on parts of React 18.3.1 ReactFiberHooks.new.js for our investigation, rather than reviewing the entire source code.
Since the source code is quite complex, we'll focus on useState as an example to gradually explore the implementation logic and data structure of Hooks. Readers interested in useEffect and other APIs can explore those on their own.
First, searching for the useState keyword, we find that there are corresponding functions for Mount (first render), Update (data update), and Rerender (render again), which are mountState, updateState, and rerenderState:
// At line 2427
const HooksDispatcherOnMount: Dispatcher = {
  ......,
  useState: mountState,
  ......
};
// At line 2454
const HooksDispatcherOnUpdate: Dispatcher = {
  ......,
  useState: updateState,
  ......
}
// At line 2458
const HooksDispatcherOnRerender: Dispatcher = {
  ......,
  useState: rerenderState,
  ......
}
Let's focus on the first mountState function to see the core logic or data structure. To focus on the core logic, I've removed the TypeScript content:
// At line 1505
function mountState(initialState){
  const hook = mountWorkInProgressHook(); // Most interesting hook data
  // Handle initial value
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // Create update queue
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  // Create dispatch function, which is the commonly used setState
  const dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ));
  return [hook.memoizedState, dispatch];
}
From this code, we find that a key element is how the hook data is created and structured. The subsequent logic is mostly about adding more data to the hook. The hook is created by mountWorkInProgressHook(), so let's look at that function:
// At line 636
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
From the code above, we can see that a hook is an object that contains a next property, which suggests it's a node in a Linked List. We can infer that Hooks data might be stored in a Linked List structure. We can confirm this by examining more code and looking at the workInProgressHook data:
export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
|};
// Hooks are stored as a "Linked list" on the fiber's memoizedState field.
// The current hook list is the list that belongs to the current fiber.
// The work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
The source code comments even directly tell us the answer, so we can confirm:
Hooks data is stored in a Linked List structure.
Here's a very concise introduction to Linked Lists:
A Linked List is a data structure used to store a sequence of elements. Each element in the sequence is called a node, and each node references (points to) the next node in the sequence.
Conceptually, it looks something like this, with a key point being that it has order and directionality:
head                             tail
 ↓                                ↓
+-----+    +-----+    +-----+    +-----+
|DATA|  -> |DATA|  -> |DATA|  -> |DATA|  -> null
+-----+    +-----+    +-----+    +-----+
  ↑           ↑          ↑          ↑
First node  Second node  Third node  Fourth node                  
So when the code below is rendered for the first time:
function Counter() {
  const [count, setCount] = useState(0); // Hook1
  const [text, setText] = useState('Count'); // Hook2
  return (
    <div>{text}: {count}</div>
  )
}
The data structure of Hooks conceptually looks like this:
Hook1 = {
  ......,
  memoizedState: 0, // count state
  next: ---> Hook2 = {
              ......,
              memoizedState: 'Count', // text state
              next: ---> null
            }
}
This includes the order and directionality of the Linked List data structure, which is a very important point.
To summarize the most important conclusion at this point: Hook nodes are stored in a Linked List data structure.
This structure allows React to maintain the relationship between Hooks and their corresponding states based on the order of Hook calls during each render — after creating the Linked List structure during the first render, subsequent update renders simply follow the same order to access this list, ensuring that each Hook can access/update its correct state.
Understanding Hooks Data Structure During Execution by Implementing a Simple useState
Since the React Hooks source code is quite complex, and we now understand the core data structure and implementation concept of Hooks, to better focus on the issue of "why Hooks must be called at the top level," I'll implement a simple version of useState using the Linked List data structure to simulate the creation and update logic of Hooks, making it easier to understand "why Hooks must be called at the top level."
p.s. The implementation below is mainly to help understand how Hooks work with Linked List data structures and changes, and doesn't fully correspond to React's actual implementation.
Implementing useState for the Mount Phase
First, let's implement a useState with just the Mount first-render functionality:
/** Implementing simple useState with Linked List structure (Mount only)**/
// Current working hook data node pointer, always points to the latest node
let workInProgressHook = null; 
function useState(initialState) {
  // Create hook node, data includes:
  // 1. memoizedState: stored state value
  // 2. next: pointer to the next node
  let hook = { 
    memoizedState: initialState,
    next: null
  };
  
  // Logic for first useState call:
  // Initialize current work node to the latest hook, no need to specify next yet
  if (workInProgressHook === null) { 
    workInProgressHook = hook; 
  } else { 
  // Logic for subsequent useState calls:
  // 1. Point the current work node's next to the newly created hook
  // 2. Set the current work node to the newly created hook
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }
  return [hook.memoizedState]; // Update (setState) functionality not yet implemented
}
export default useState;
The usage is the same as React's useState, but since we've simplified the logic of useState, it's easier to understand "how useState actually works when executed."
Here's a simple implementation of a Counter component. Try to mentally trace through the flow of useState operations during the rendering of Counter and the resulting Hooks data structure after rendering:
import useState from './simpleUseState.js';
function Counter() {
  const [isShowText] = useState(false); // Hook1
  const [text] = useState('Count'); // Hook2
  const [count] = useState(0); // Hook3
  return (
    <div>
      <div>
        {isShowText && `${text}: `}{count}
      </div>
      ......
    </div>
  )
}
The flow of useState operations during rendering:
- Hook1 
useState(isShowText) executes- Creates hook1 node, 
memoizedStateis false,nextis null workInProgressHookis set to hook1
 - Creates hook1 node, 
 - Hook2 
useState(text) executes- Creates hook2 node, 
memoizedStateis 'Count',nextis null - workInProgressHook(hook1)'s next points to hook2, then 
workInProgressHookis set to hook2 
 - Creates hook2 node, 
 - Hook3 
useState(count) executes- Creates hook3 node, memoizedState is 0, next is null
 - workInProgressHook(hook2)'s next points to hook3, then 
workInProgressHookis set to hook3 
 
The Linked List data structure after rendering conceptually looks like this:
(Hooks Linked List data structure concept after Mount)
If you haven't yet understood the concepts and code above, I recommend going back and reviewing them until you do, as we're about to move from the "Mount" phase to the "Update" phase, which will be more complex.
Adding the Update Mechanism to useState
Before modifying the useState code, let's recall the basic logic of React's state update mechanism:
- After Hook1's 
useStateexecutes, it returns asetStateAPI, allowing Hook1 to update itsstate - When Hook1's 
setStateexecutes, it updates Hook1'sstate, but doesn't affect Hook2 or Hook3'sstate; in other words, Hook2 and Hook3'sstateneed to maintain their previous results. - After the 
stateupdates, the component re-renders. 
From this logic, we can identify something important: we need to record the previous Hooks results. This allows us to ensure that when updating Hook1's state, Hook2 and Hook3's states remain their previous state values.
Therefore, we need to add the following data and logic:
- Add 
storedHook: to save the Hooks results from the previous render. - Add 
firstWorkInProgressHook: to save the first node ofworkInProgressHook, making it easier to assign the initial node tostoredHook. The implementation logic below will make this clearer. - Add logic to handle the "Update" flow, which needs to be distinguished from "Mount"
 
/** Implementing simple useState with Linked List structure (with Mount and Update) **/
let workInProgressHook = null; // Current working hook linked list data
let firstWorkInProgressHook  = null; // Save the first node of workInProgressHook
let storedHook = null; // Save the hook linked list data from the previous render
 
function useState(initialState) {
  let hook;
  // Check if it's the Mount or Update phase
  const isMounted = storedHook === null 
  // Mount flow: assign brand new data to hook
  if(isMounted) { 
    hook = {
      memoizedState: initialState,
      next: null
    };
  } else {
  // Update flow: reuse state from the previous render's hook
    hook = {
      memoizedState: storedHook.memoizedState,
      next: null
    };
    // After processing this node, move to the next node
    storedHook = storedHook.next;
  }
  if (workInProgressHook === null) { 
    workInProgressHook = hook; 
    // Set firstWorkInProgressHook
    firstWorkInProgressHook = hook;
  } else { 
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }
  // setState implementation
  const setState = (newState) => {
    // Update hook's memoizedState
    hook.memoizedState = typeof newState === 'function' 
    ? newState(hook.memoizedState) 
    : newState;
    // Store this round's hook linked list for the next render
    storedHook = firstWorkInProgressHook;
    // Reset the current working hook linked list before re-rendering
    workInProgressHook = null;
    firstWorkInProgressHook = null;
    // Assuming this would trigger a re-render, causing the component to execute again for the next render
    console.log('State updated, would trigger re-render component.');
  };
  return [hook.memoizedState, setState]; 
}
export default useState;
Now our useState provides setState functionality to update Hook data. It can be used like this:
import useState from './simpleUseState.js';
function Counter() {
  const [isShowText, setIsShowText] = useState(false); // Hook1
  const [text, setText] = useState('Count'); // Hook2
  const [count, setCount] = useState(0); // Hook3
  return (
    <div>
      <div>
        {isShowText && `${text}: `}{count}
      </div>
      {/* Update data using setIsShowText */}
      <button onClick={() => setIsShowText(prev => !prev)}> 
        {isShowText ? 'Hide Label' : 'Show Label'}
      </button>
      ......
    </div>
  )
}
Now let's think about how the program works, analyzing step by step what happens during the transition from Mount to Update phase and the conceptual structure of the Hooks data.
Let's start with the simpler Mount phase. The most obvious difference is the addition of firstWorkInProgressHook:
【First Render Mount Flow】
- Hook1 
useState(isShowText) executes- Creates hook1 node, enters Mount logic, 
memoizedStateis false;nextis null - At this point 
workInProgressHookis null, soworkInProgressHookis set to hook1, andfirstWorkInProgressHookis also set to hook1 
 - Creates hook1 node, enters Mount logic, 
 - Hook2 
useState(text) executes- Creates hook2 node, enters Mount logic, 
memoizedStateis 'Count';nextis null - workInProgressHook(hook1)'s next points to hook2, then 
workInProgressHookis set to hook2 
 - Creates hook2 node, enters Mount logic, 
 - Hook3 
useState(count) executes- Creates hook3 node, enters Mount logic, 
memoizedStateis 0;nextis null - workInProgressHook(hook2)'s next points to hook3, then 
workInProgressHookis set to hook3 
 - Creates hook3 node, enters Mount logic, 
 
(Hooks data structure concept after Mount, firstWorkInProgressHook points to the first node)
Now let's explore the relatively more complex Update flow. Each step will include a conceptual diagram of the Hooks data structure:
【When the user clicks the button, triggering setIsShowText(prev => !prev) Update Flow】
- Hook1's 
setStateexecutes- Changes hook1's 
memoizedStatefrom false to true - Sets 
storedHooktofirstWorkInProgressHook, storing the Hooks from the previous render, note that the stored previous render Hooks only contain Hook1 and Hook3 nodes, not Hook2 - Resets 
workInProgressHookandfirstWorkInProgressHookto null in preparation for re-rendering - Triggers re-render, re-executing the component logic!
 
 - Changes hook1's 
 
(Hooks data concept after setState execution, working Hooks are cleared, and previous Hooks structure is stored)
From the data concept diagram, we can see: the working Hooks are cleared, with firstWorkInProgressHook and workInProgressHook pointing to null; meanwhile, a set of stored Hooks has been created to preserve the previous render's Hooks, with storedHook pointing to the head of the stored Hooks. Now let's move to the execution of the first useState:
- Hook1 
useState(isShowText) executes- Creates hook1, since 
storedHookis not null, enters Update flow - Sets hook1's 
memoizedStatetostoredHook.memoizedState - Sets 
storedHooktostoredHook.next, meaning storedHook data changes from the previous round's hook1 to the previous round's hook3, note that "storedHook points to hook3 instead of hook2, because hook2 doesn't exist in the previous render" - At this point 
workInProgressHookis null, soworkInProgressHookis set to hook1, andfirstWorkInProgressHookis also set to hook1 
 - Creates hook1, since 
 
(Hooks data concept after the first isShowText useState execution)
From the data concept, we can see: the working Hooks have created a Hook1 node, pointed to by both firstWorkInProgressHook and workInProgressHook; meanwhile, storedHook now points to the stored Hook3 node. Now let's move to the execution of the second useState:
- Hook2 
useState(text) executes- Creates hook2, since 
storedHookis not null, enters Update flow - Sets hook2's 
memoizedStatetostoredHook.memoizedState - Sets 
storedHooktostoredHook.next, meaning storedHook data changes from the previous round's hook3 to the previous round's null tail - workInProgressHook(hook1)'s next points to hook2, then 
workInProgressHookis set to hook2 
 - Creates hook2, since 
 
(Hooks data concept after the second text useState execution)
From the data concept, we can see: the working Hooks have created a Hook2 node, pointed to by workInProgressHook; meanwhile, storedHook now points to null, meaning there are no more stored Hooks.
Through the Hooks data structure concept diagrams after each step, we can better understand the current state of the data. However, so far we've only been showing what happens when Hooks are "correctly used." This seems normal, but what problems would arise if we don't call Hooks at the top level?
What Problems Arise When Breaking the "Hooks Must Be Called at the Top Level" Rule
Now that we understand the data structure of Hooks and how the data changes during execution, let's move on to the more interesting part: what happens when we break the rules for using Hooks?
What Happens if We Add Conditions When Using useState?
Let's use the following incorrect code as an example to see what problems arise during execution. We'll focus on what happens when we add conditions to useState:
import useState from './simpleUseState.js';
import ToggleButton from './ToggleButton.js';
function Counter() {
  const [isShowText, setIsShowText] = useState(false); // Hook1
  /** Hook incorrectly added with condition **/
  if(isShowText) {
    const [text, setText] = useState('Count'); // Hook2
    return (
      <div>
        <div>{text}</div>
        <ToggleButton 
          label='Show Count'
          onClick={() => setIsShowText(prev => !prev)} 
        />
        ......
      </div>
    )
  }
  const [count, setCount] = useState(0); // Hook3
  return (
    <div>
      <div>{count}</div>
      <ToggleButton 
        label='Show Text'
        onClick={() => setIsShowText(prev => !prev)} 
      />
      ......
    </div>
  )
}
The key point is that Hook2 (text variable) won't be created during the Mount phase, it will be skipped!
【First Render Mount Flow】
- Hook1 
useState(isShowText) executes- Creates hook1 node, enters Mount logic, 
memoizedStateis false;nextis null - At this point 
workInProgressHookis null, soworkInProgressHookis set to hook1, andfirstWorkInProgressHookis also set to hook1 
 - Creates hook1 node, enters Mount logic, 
 - "Because isShowText is false, Hook2 
useState(text) execution is skipped" - Hook3 
useState(counte) executes- Creates hook3 node, enters Mount logic, 
memoizedStateis 0;nextis null - workInProgressHook(hook1)'s next points to hook3, then 
workInProgressHookis set to hook3 
 - Creates hook3 node, enters Mount logic, 
 
After Mount, the Hooks data structure concept looks like this:
(Hooks concept after Mount when Hook2 useState is placed in if/else, Hook2 node is not created)
No problems have occurred during the Mount phase. However, what happens when we move to the Update phase? Will any problems occur?
【When the user clicks the button, triggering setIsShowText(prev => !prev) Update Flow】
- Hook1's 
setStateexecutes- Changes hook1's 
memoizedStatefrom false to true - Sets 
storedHooktofirstWorkInProgressHook, storing the Hooks from the previous render, note that the stored previous render Hooks only contain Hook1 and Hook3 nodes, not Hook2 - Resets 
workInProgressHookandfirstWorkInProgressHookto null in preparation for re-rendering - Triggers re-render, re-executing the component logic!
 
 - Changes hook1's 
 
(Hooks concept after Hook1 setState)
After completing the first step of setState update, the Hooks data still hasn't shown any obvious problems. Now let's move to the execution of Hook1:
- Hook1 
useState(isShowText) executes- Creates hook1, since 
storedHookis not null, enters Update flow - Sets hook1's 
memoizedStatetostoredHook.memoizedState - Sets 
storedHooktostoredHook.next, meaning storedHook data changes from the previous round's hook1 to the previous round's hook3, note that "storedHook points to hook3 instead of hook2, because hook2 doesn't exist in the previous render" - At this point 
workInProgressHookis null, soworkInProgressHookis set to hook1, andfirstWorkInProgressHookis also set to hook1 
 - Creates hook1, since 
 
(Hooks concept after Hook1 useState executes again)
As a reminder, the most important thing to note in this step is: storedHook now points to the Hook3 data node! Not the Hook2 data node, because the Hook2 data node hasn't been created yet! Next, we'll move to the Hook2 useState execution:
- Because isShowText is true, Hook2 (text) 
useStatewill execute, but a problem will occur!- Creates hook2, since 
storedHookis not null, enters Update flow - Sets hook2's 
memoizedStatetostoredHook.memoizedState, at this point storedHook is the previous round's hook3 => Problem occurs! This means Hook2 (text)'s data will be incorrectly set to Hook3 (count)'s data 0 - Sets 
storedHooktostoredHook.next, meaning storedHook data changes from the previous round's hook3 to the previous round's null tail - workInProgressHook(hook1)'s next points to this round's newly created hook2, then 
workInProgressHookis set to hook2 
 - Creates hook2, since 
 
(Hooks concept after Hook2 useState executes again)
At this step, we can see a major problem: Since the Mount phase didn't have a Hook2(text) node, only a Hook3(count) node, during the Update phase, Hook2(text)'s data is directly set to the Mount phase's Hook3(count) data, causing what should be 'Count' to become 0.
Through this example simulating React Hooks creation and update, we can understand why Hooks can't be placed in conditions:
Because React Hooks are stored sequentially in a Linked List structure, if certain Hooks are skipped during the Mount phase due to conditional logic, it will lead to inconsistency in the Hook node order during the Update phase, causing incorrect mapping of state data and producing serious bugs.
Of course, I've only simulated a very basic concept of React Hooks here. In reality, React does much more complex processing logic and rendering flow, but in terms of the most important data logic and concepts, this explanation adequately represents "why Hooks have the rule that they can't be placed in conditions."
What Happens if We Add Loops When Using useState?
Once we understand the data logic and structure of React Hooks implementation, we can understand more rules related to "Hooks must be called at the top level," such as not being able to place React Hooks in loops.
Let's again use our simple version of useState to write some incorrect code, placing useState inside a loop:
import useState from './simpleUseState.js';
function TodoList() {
  const [todos, setTodos] = useState(['Task 1', 'Task 2']); // Hook1
  /** Hook incorrectly placed in a loop **/
  todos.map((todo) => {
    // Will generate (todos.length - 1) Hooks
    // Generated Hook2, Hook3 after mounted
    const [isDone, setIsDone] = useState(false); // Hook2, Hook3
    return (
      <div>
        <span style={{ textDecoration: isDone ? 'line-through' : 'none' }}>
          {todo}
        </span>
        <button onClick={() => setIsDone(prev => !prev)}>
          {isDone ? 'Undo' : 'Done'}
        </button>
      </div>
    )
  })
  const [newTodo, setNewTodo] = useState(''); // Hook4 after mounted
  return (
    <div>
      <input
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button onClick={() => {
        setTodos(prev => [...prev, newTodo]);
        ......
      }}>
        Add Todo
      </button>
    </div>
  )
}
In this code logic, we can observe a key point: the number of Hooks is determined by the length of Todos, so it's a variable state. This logic is quite important; keep it in mind.
Since we've already discussed many useState execution steps, I won't detail the Mount steps; let's go straight to the Hooks data structure concept after Mount:
(Hooks concept after Mount when useState is placed in a loop)
We can see that because there are currently two Todos, after the map loop completes, two Hooks nodes are generated.
During the Mount phase, no obvious problems have occurred yet. Problems will arise during the Update phase, for example, when adding a new Todo item:
【When the user triggers setTodos(prev => [...prev, newTodo]) Update Flow】
- Hook1's 
setStateexecutes- Changes hook1's 
memoizedStatefrom ['Task 1', 'Task 2'] to ['Task 1', 'Task 2', 'Task 3'] - Sets 
storedHooktofirstWorkInProgressHook - Resets 
workInProgressHookandfirstWorkInProgressHookto null in preparation for re-rendering - Triggers re-render, re-executing the component logic!
 
 - Changes hook1's 
 
(Hooks concept after adding a new Todo item and setState execution)
Next, the re-rendering execution logic begins. During re-rendering, because the Todos array has an additional element, the loop will execute one more time, which will cause a serious problem:
- Hook1 
useState(todos) executes: smoothly updates, refer to previous cases if you're unsure about the steps 
(Hooks concept after the first useState for todos data executes again)
- "Because there are now three elements in Todos, 
mapwill executeuseStatethree times! This is inconsistent with the previous two executions ofuseState, causing problems!"- First loop iteration: This round's hook2 (isDone) correctly uses data from 
storedHook's hook2 (isDone), no problem - Second loop iteration: This round's hook3 (isDone) correctly uses data from 
storedHook's hook3 (isDone), no problem - Third loop iteration: This round's hook4 (isDone) will incorrectly use data from 
storedHook's hook4 (newTodo), error occurs! This causes the new round's hook4 isDone to incorrectly use newTodo's data! 
 - First loop iteration: This round's hook2 (isDone) correctly uses data from 
 
(Hooks concept after multiple isDone useState in the loop execute again)
Now we can see that placing Hooks in a map or other loop structure will indeed cause major problems.
Because React Hooks are stored in a Linked List structure sequentially, if Hooks are used in loops, the number of Hook nodes generated during each render will vary based on the loop iteration count. This will lead to a mismatch in the number of Hook nodes produced in the new render round compared to the previous one, breaking the correspondence between Hooks and causing serious bugs.
By the way, if we want to rewrite this code to avoid problems, we can take a few approaches:
- Method 1: Try to encapsulate the 
isDonedata directly intodos, so eachtodohas its ownisDoneproperty, eliminating the need for a separateuseStatedeclaration forisDone - Method 2: Try to extract a Todo component and declare the useState with 
isDonedata at the top level of the new component, which would comply with the rule of using Hooks at the top level of components. 
This section has only focused on the "conditions" and "loops" aspects of "Do not call Hooks inside conditions or loops." However, many other related rules are also related to the data logic concept of React Hooks, such as "Do not call Hooks inside try/catch/finally blocks" and "Do not call Hooks in event handlers," which are listed in the React official documentation. If you're interested, you can extend the same concepts to think about these cases.
Conclusion: Remember to Call Hooks at the Top Level
Through this article's exploration of the data concept and logic of React Hooks implementation, we can better understand why the React official documentation emphasizes the "Only call Hooks at the top level" rule. It is indeed related to the implementation logic behind React Hooks. Here are some key conclusions:
- React Hooks Data Structure
- Hooks use a Linked List structure to store state
 - Each Hook is conceptually a node in the Linked List
 - Hook nodes are connected through the 
nextpointer, forming an ordered data structure 
 - Why Can't We Use Hooks in Conditions?
- Conditional judgments might cause some Hook nodes to be skipped during first render, not being created
 - Due to the sequential nature of the Hooks data structure, this will cause confusion in the Hook correspondence during subsequent updates
 - Eventually, Hook states in conditionals might be assigned incorrect values, creating unpredictable bugs
 
 - Why Can't We Use Hooks in Loops?
- The number of Hooks in loops might change with the iteration count
 - This dynamic change in Hook quantity breaks the stability of the Linked List
 - Eventually, some Hook states might be incorrectly mapped to other Hook data, creating unpredictable bugs
 
 
Overall, this understanding process not only satisfies curiosity about the underlying principles but also helps developers better understand the design and limitations of data logic. When encountering similar data logic or implementations in the future, developers can quickly recognize what limitations exist.
However, in practical development, as long as you properly use the ESLint rule eslint-plugin-react-hooks, you can detect problems during the development phase and avoid violating this rule that requires calling Hooks at the top level. So using ESLint fully is very important. Remember to include Lint jobs in the CICD items that must run before each release to ensure that all code from the project's developers is constrained within these rules.