Reactのアーキテクチャを知る useState編3

こんにちは、noyanです。React内部実装アドベントカレンダーの4日目です。昨日集中しすぎてうまく頭が働かないようなので、文章が荒かったらすみません。

引き続きReactのuseStateフックの内部実装を読み解いていきます。

これまで、マウント時のuseState()の挙動とsetStateにバインドされる関数を見てきました。今回は、setStateが呼び出された時、actionがどうやってstateに反映されるのかを見ていきます。

この記事が読み終わった時、setState(prev => prev+1)がどう処理されるのかを理解できることをゴールとします。

queueの行き先

setStateの正体であるdispatchSetStateは、前回の記事で見たところhook.queueにactionをenqueueしていました。このhook.queueはどの関数でstateに追加されるのでしょうか。

queueの更新はrerenderReducerで行われます。queueの構造を思い出しながら読んでください。

function rerenderReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  // This is a re-render. Apply the new render phase updates to the previous
  // work-in-progress hook.
  const dispatch: Dispatch<A> = (queue.dispatch: any);

  // queue.pendingは一番最後の更新、pending.nextは最初の更新が入っていた
  const lastRenderPhaseUpdate = queue.pending;
  // hooks.memoizedStateは現在DOMに反映されているhookの値
  let newState = hook.memoizedState;
  if (lastRenderPhaseUpdate !== null) {
    // The queue doesn't persist past this render pass.
    queue.pending = null;

    const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;
    let update = firstRenderPhaseUpdate;
      

    do {
      // Process this render phase update.
      const action = update.action;
      // setState(action)に等しい
      newState = reducer(newState, action);
      // 次のupdateに行き、循環リストが一周するまでnewStateを更新し続ける。
      update = update.next;
    } while (update !== firstRenderPhaseUpdate);

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    // Don't persist the state accumulated from the render phase updates to
    // the base state unless the queue is empty.
    if (hook.baseQueue === null) {
      hook.baseState = newState;
    }

    queue.lastRenderedState = newState;
  }
  return [newState, dispatch];
}

stateの更新だけに限って話すと、以下が概要です。

do {
  newState = basicStateReducer(newState, update.action);
  update = update.next;
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

つまり、setStateで入れられたqueueはrerenderReducerによって順番に適応され、新しい値がhook.memoizedStateに代入されます。useStateで返されるstateはhook.memoizedStateを参照しているので、コンポーネントが再計算されたときにこの値が取得できるわけです。

この再計算がどこで行われるかというと、初日の記事で読んだrenderWithHooksです。明日はrenderWithHooksを中心にuseStateの総ざらいをしたいと思います。