How React Hooks Work

How React Hooks Work

Description
React Hooks are a revolutionary feature introduced in React 16.8, allowing function components to use state and other React features (such as lifecycle). To understand how they work, the key is to figure out: after a function component is invoked, its execution context ends, so how do Hooks (like useState) persist and associate the correct state across multiple renders?

Problem-Solving Process

  1. Core Issue: Associating State with Components

    • For class components, state (this.state) is stored on the component instance (this). The instance exists throughout the component's lifecycle, thus the state is preserved.
    • For function components, it's just a function. Each render (or re-render) is a completely new function call. After the function executes, its internal local variables are typically destroyed. Therefore, the state returned by useState must be stored somewhere outside the function component, and there needs to be a mechanism to associate it with the specific component function and the specific Hook call.
  2. Answer: React's Maintenance Mechanism

    • React internally maintains a universal data structure to store state, side effects, and other information for all components. For function components, React uses a data structure called "Fiber" to represent a unit of work. Each function component corresponds to a Fiber node.
    • In the Fiber node, there is a property called memoizedState. It's not a single value, but a linked list data structure.
  3. Linked List Structure of Hooks

    • When you call multiple Hooks within a function component (e.g., two useState and one useEffect), React does not distinguish them by variable names but strictly records them according to the order in which they are called within the component.
    • During the component's first render (mount):
      • React initializes an empty memoizedState linked list for this component's Fiber node.
      • When the first Hook is executed (e.g., const [name, setName] = useState('Alice')), React creates an object representing this Hook (often called a "Hook object"). This object contains the state value (e.g., 'Alice'), a queue for updating the state (dispatch), and other information.
      • React attaches this Hook object as the first node of the linked list to memoizedState.
      • When the second Hook is executed (e.g., const [age, setAge] = useState(30)), React creates a second Hook object and links it as the second node after the first one.
      • This process continues. Other Hooks like useEffect and useCallback are added to the linked list in the same way, following the order of invocation.
    • During subsequent update renders of the component:
      • React executes the function component again.
      • When it encounters useState again, it does not create a new Hook object. Instead, it traverses the Hook linked list on the component's Fiber node, retrieving the corresponding Hook objects one by one in order, and returns the current state value stored within them.
      • This process entirely depends on the invocation order. If the Hook call order in the second render differs from the first, React, following the linked list, would incorrectly associate the state of the first Hook with the second Hook, causing bugs.
  4. Why the Hook Call Order Cannot Change (Hooks Rules)

    • Based on the above principle, it's easy to understand the rules proposed by the React team: "Only Call Hooks at the Top Level" and "Don’t Call Hooks inside loops, conditions, or nested functions".
    • Example: Suppose we have the following code:
      function MyComponent({ isVisible }) {
        if (isVisible) {
          const [name, setName] = useState('Alice'); // First Hook
        }
        const [age, setAge] = useState(30); // Second Hook
      }
      
      • First render: isVisible is true.
        • Execution order: Hook1 (useState('Alice')) -> Hook2 (useState(30)).
        • React internal list: [Hook1(name), Hook2(age)].
      • Second render: isVisible changes to false.
        • Execution order: Skips the if block directly, executes Hook2 (useState(30)).
        • What does React do internally? It traverses the list in order: it expects the first Hook to be useState('Alice'), but the first Hook encountered this time is actually useState(30). React will incorrectly return the state from linked list node 1 (storing 'Alice') to the first Hook call of this render (which was supposed to be for age), causing serious data corruption.
  5. State Updates and Triggering Re-renders

    • When you call the setter function returned by useState (e.g., setName('Bob')), React places the update action (action, i.e., 'Bob') into the update queue of that Hook object.
    • Then, React schedules a new render.
    • During the new render, the component function is executed again. When useState is encountered, React finds the corresponding Hook object from the linked list, calculates the latest state value from its update queue, and returns this new value to you. This way, the component renders the UI with the new state.

Summary
The core principle of how React Hooks work is: React stores all Hook states and information in a linked list on the component's Fiber node, maintaining them in the order of invocation. Each time the component renders, React traverses this list, providing the corresponding state or executing the corresponding side effects in a fixed order. This perfectly explains why Hooks enable functions to have "memory" and also explains why the stability of Hook call order must be guaranteed.