Why don’t function components before react 16 have state?

As you know, function components did not have state before react 16, and component state could only be passed through props.

Write two simple components, a class component and a function component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const App = () =><span>123</span>;

class App1 extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      a: 1,
    }
  }
  render() {
    return (<p>312</p>)
  }
}

Compile App1 with babel, and App1 is a function component after compilation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 伪代码
var App1 = /*#__PURE__*/function (_React$Component) {
  _inherits(App1, _React$Component);

  var _super = _createSuper(App1);

  function App1(props) {
    var _this;

    _classCallCheck(this, App1);

    _this = _super.call(this, props);
    _this.state = {
      a: 1
    };
    return _this;
  }

  _createClass(App1, [{
    key: "render",
    value: function render() {
      return/*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
        children: "312"
      });
    }
  }]);

  return App1;
}(React.Component);

So why don’t function components have state? The difference between a function component and a class component is whether or not there is a render method on the prototype. react calls the render method of the class component when it renders. The render of a function component is the function itself, and after it is executed, the internal variables are destroyed, so when the component is re-rendered, the previous state is not available. Unlike function components, class components generate an instance of a class component when they are rendered for the first time, and the render method is called render. When it is re-rendered, the instance reference of the class component is obtained and the corresponding method of the class component is called at a different life cycle.

There is also no difference between the class component and the function component in terms of their data structures after rendering.

react

Why do function components have state after react 16?

As we all know, the biggest change made in react 16 is the fiber, and the data structure of the node (fiber node) has been changed significantly to fit the fiber. Modify the App component, render it on the page, and get the fiber node data structure as shown below.

1
2
3
4
5
const App = () => {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(1);
  return<span>123</span>
};

fiber node

(function component on the left, class component on the right)

How does react know which component the current state belongs to?

All function component states are injected through useState, how do you identify the corresponding component?

A breakpoint in the render flow of react shows that function components have a special render method renderWithHooks. The method has 6 parameters: current, workInProgress, component, props, secondArg, nextRenderExpirationTime.

1
2
3
4
5
6
current: 当前正在页面渲染的node,如果是第一次渲染,则为空
workInProgress: 新的node,用于下一次页面的渲染更新
component: node对应的组件
props: 组件的props
secondArg: 不清楚...,不影响后续文章阅读
nextRenderExpirationTime: fiber渲染的过期时间

When executing renderWithHooks, the current fiber node is recorded with the variable currentlyRenderingFiber$1. So when the function component is executed, the useState method gets the state of the current node. The state is inserted into the memoizedState field of the corresponding node. The returned method that triggered the state change also knows which fiber node it is when the change is executed because of the closure. The corresponding source code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function mountState(initialState) {
  // 获取hook状态
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  // 绑定当前node和更新队列
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

renderWithHooks is only used for rendering function components.

The value of the memoizeState field shows that the state of the function component and the class component store different data structures. Class components are simple data objects, while function components are one-way chained tables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface State {
    memoizedState: state数据和baseState值相同,
  baseState: state数据,
  baseQueue: 本次更新之前没执行完的queue,
  next: 下一个state,
  queue: {
    pending: 更新state数据这个数据是一个对象里面有数据还有其他key用于做其他事情。),
    dispatch: setState方法本身,
    lastRenderedReducer: useReducer用得上,
    lastRenderedState: 上次渲染的State.memoizedState数据,
  }
}

What happens when the setA method is called?

Before we talk about updating the component state, let’s look at the flow of the component mount.

state

When useState is called, the currentlyRenderingFiber$1 is used to get the fiber node of the current component and mount the data to the memoizedState field on the node. This way the function component has a state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// react
function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function resolveDispatcher() {
  // ReactCurrentDispatcher 的值是react-dom注入的,后续会讲。
  var dispatcher = ReactCurrentDispatcher.current;

  if (!(dispatcher !== null)) {
    {
      throwError( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
    }
  }

  return dispatcher;
}


// react-dom 会根据当前组件的状态注入不同的useState实现方法,这里可以先忽略。
useState: function (initialState) {
  currentHookNameInDev = 'useState';
  mountHookTypesDev();
  var prevDispatcher = ReactCurrentDispatcher.current;
  ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;

  try {
  // 挂载state
    return mountState(initialState);
  } finally {
    ReactCurrentDispatcher.current = prevDispatcher;
  }
},

function mountState(initialState) {
  // 生成hook初始化数据,挂到fiber node节点上
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };

  if (workInProgressHook === null) {
    // node节点的memoizedState指向第一个hooks
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    // 上一个hooks的next,等于当前hooks,同时把当前workInProgressHook,等于当前hooks
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

useState also returns the corresponding state and a method to modify state. The method dispatchAction that modifies state is bound to the current fiber node, along with the action queue that updates the current state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 这里删除了部分无关代码
function dispatchAction(fiber, queue, action) {
  // 这些都是用于Fiber Reconciler,在这里不用太在意
  var currentTime = requestCurrentTimeForUpdate();
  var suspenseConfig = requestCurrentSuspenseConfig();
  var expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);
  var update = {
    expirationTime: expirationTime,
    suspenseConfig: suspenseConfig,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  {
    update.priority = getCurrentPriorityLevel();
  }


  // pending 是当前state是否有未更新的任务(比如多次调用更新state的方法)
  var pending = queue.pending;

  // queue是一个循环链表
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;
  var alternate = fiber.alternate;

  if (fiber === currentlyRenderingFiber$1 || alternate !== null &amp;&amp; alternate === currentlyRenderingFiber$1) {
    // Reconciler 计算是否还有时间渲染,省略
  } else {
    // 此处省略很多代码
    // 标记当前fiber node需要重新计算。
    scheduleWork(fiber, expirationTime);
  }
}

As you can see from the above code, when the setA method is called to update the component state, the data to be updated is generated, wrapped in a data structure and pushed to the queue in the state.

scheduleWork will trigger the react update, so that the component needs to be re-rendered. The overall process is basically the same as when it was first mounted, but the implementation of the mountState method body shows that the component is rendered using initialState. This is definitely problematic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function mountState(initialState) {
  // 挂载state
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  // state的初始值是initialState,也就是组件传入的值
  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

From this, we can infer that in the previous step, there must be an implementation method that indicates that the current component is not initially mounted and needs to be replaced with useState. The answer is found in renderWithHooks.

To make it easier to understand, there are two key pieces of data in react: current and workInProgress, which represent the fiber node rendered by the current page, and the fiber node that calculates the difference after the update is triggered. for rendering.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 这里删除部分无关代码

// current 当前页面上组件对应的fiber node
// workInProgress 当前重新渲染对应的fiber node
// Component 函数方法体
// ...
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
  // currentlyRenderingFiber$1 是当前正在渲染的组件,后续渲染流程会从改变量获取state
  currentlyRenderingFiber$1 = workInProgress;


  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork; // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;
  // didScheduleRenderPhaseUpdate = false;
  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because memoizedState === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)
  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so memoizedState would be null during updates and mounts.

  {
    // 如果当前current不为null,且有state,说明当前组件是更新,需要执行的更新state,否则就是初次挂载。
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } elseif (hookTypesDev !== null) {
      // This dispatcher handles an edge case where a component is updating,
      // but no stateful hooks have been used.
      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
      // but with the extra DEV validation to ensure hooks ordering hasn't changed.
      // This dispatcher does that.
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  }

  // 往后省略
}

In the renderWithHooks method, the ReactCurrentDispatcher is modified, which results in a different method body corresponding to useState. The useState method call in HooksDispatcherOnUpdateInDEV is updateState. This method ignores initState and chooses to get the current state from the state of the fiber node.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
useState: function (initialState) {
  currentHookNameInDev = 'useState';
  updateHookTypesDev();
  var prevDispatcher = ReactCurrentDispatcher.current;
  ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;

  try {
    return updateState(initialState);
  } finally {
    ReactCurrentDispatcher.current = prevDispatcher;
  }
},

function updateState(initialState) {
  return updateReducer(basicStateReducer);
}

function updateReducer(reducer, initialArg, init) {
  // 根据之前的state初始化新的state结构,具体方法在下面
  var hook = updateWorkInProgressHook();
  // 当前更新state的队列
  var queue = hook.queue;

  queue.lastRenderedReducer = reducer;
  var current = currentHook; // The last rebase update that is NOT part of the base state.

  var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.

  var pendingQueue = queue.pending;

  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    if (baseQueue !== null) {
      // Merge the pending queue and the base queue.
      var baseFirst = baseQueue.next;
      var pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    // We have a queue to process.
    var first = baseQueue.next;
    var newState = current.baseState;
    var newBaseState = null;
    var newBaseQueueFirst = null;
    var newBaseQueueLast = null;
    var update = first;

    do {
      // fiber Reconciler 的内容,省略
      } else {
        // This update does have sufficient priority.
        if (newBaseQueueLast !== null) {
          var _clone = {
            expirationTime: Sync,
            // This update is going to be committed so we never want uncommit it.
            suspenseConfig: update.suspenseConfig,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: null
          };
          newBaseQueueLast = newBaseQueueLast.next = _clone;
        } // Mark the event time of this update as relevant to this render pass.
        // TODO: This should ideally use the true event time of this update rather than
        // its priority which is a derived and not reverseable value.
        // TODO: We should skip this update if it was already committed but currently
        // we have no way of detecting the difference between a committed and suspended
        // update here.


        markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig); // Process this update.

        if (update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = update.eagerState;
        } else {
                  // 执行状态更新,reducer是个包装函数:typeof action === 'function' ? action(state) : action;
          var action = update.action;
          newState = reducer(newState, action);
        }
      }

      update = update.next;
    } while (update !== null &amp;&amp; update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    } // Mark that the fiber performed work, but only if the new state is
    // different from the current state.


    if (!objectIs(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

function updateWorkInProgressHook() {
  var nextCurrentHook;

  // 当前
  if (currentHook === null) {
    // alternate 指向的是当前页面渲染组件对应fiber node
    var current = currentlyRenderingFiber$1.alternate;

    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  var nextWorkInProgressHook;

  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    if (!(nextCurrentHook !== null)) {
      {
        throwError( "Rendered more hooks than during the previous render." );
      }
    }

    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
    };

    if (workInProgressHook === null) {
      
     // 第一个hook currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
    } else {
      // 下一个hooks,关联前一个hooks
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }

  return workInProgressHook;
}

At this point, it’s clear what react is doing internally by calling the setA method. setA inserts an update action into the queue of the current state and notifies react that there is a component state that needs to be updated. When updating, the method body of useState is different from the initial mounted method body, so when updating, it ignores the initState passed by useState, gets the initial data from the baseState of the node data, and executes the update action in the queue step by step until the queue is empty, or the queue is finished.

Why do function components sometimes get a state that is not real-time?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const App3 = () => {
  const [num, setNum] = React.useState(0);
  const add = () => {
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  };

  return (
    <>
      <div>{num}</div>
      <button onClick={add}>add</button>
    </>
  );
}

When the button is clicked within one second, no matter how many times it is clicked, the final page return will be 1. The reason: setTimeout closes the current state num, and when executing update state, the corresponding baseState has not been updated and is still old, i.e. 0, so multiple clicks will still be 0 + 1 = 1. The way to modify this is to change the argument passed in to a function, so that when react executes queue, the state value from the previous step is passed to the current function.

1
setNum((state) => state + 1);

Why can’t useState be declared in a judgment statement?

The official react website has this to say.

react

Suppose there are 3 states, A, B, C. If B is in the judgment statement, then the states of A and B will be updated in time, but C will not be updated. Because 2 calls to useState will only update state twice, in the chain of state, A.next->B, B.next->C, then only A and B will be updated, C will not be updated, leading to some unpredictable problems.

Why does state need to be associated with a linked table?

I don’t have an answer to this question, but the only thing I can parse is: it’s for everything (pure) functions, right?

state is still an object, and is updated by calling a method. This way and the class component in turn remains unified and better understood.

Conclusion

By reading the source code to understand the execution of useState, we can deepen our understanding of the react function component state update. Feel free to point out any shortcomings or mistakes.

The parsing above is based on react@16, reac-dom@16.