🦙
Deep Dive useState() (initial render 編)

mokun632

mokun632

2023年12月26日
react internals

Reactの内部実装を見ていくDeep DiveシリーズのuseStateの初期レンダリング編です。

# Deep Dive useState() (initial render 編)

React の useState()はみなさんご存知かと思います。
今回は initial render(mount)時に、 useState()が内部的にどのように動作するのかを、 ソースコードを見ながら解明していきます。
initial render はそこまで複雑ではありません。

# useState の本体

これが useState の本体になります。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

引数にある、initialState: (() => S) | Sは、useState()の引数に渡された値です。 関数だった場合、mountStateImplで実行され、state の初期値が決定されます。

return している値に注目してください。useState()で返される、おなじみの構文になっています。

mountStateImpl内で、新しい Hook の作成されたり state の初期化がされたりしています。

# React の状態更新

まず、mountStateImplの実装を見ていきましょう。

function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

先ほど、言ったように、

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

で、initialStateが関数だった場合、実行されます。

続いて、queue の部分について注目します。

const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;

React はすぐには状態更新されません。
更新は、更新キューに入れられ、非同期で処理されます。
理由としては更新の優先順位が異なる可能性があるためです。
更新の優先順位については、React Lane という概念が存在していて、それによって決定されます。

# Hook の作成

mountStateImplmountWorkInProgressHookという関数があったと思いますが、
mountWorkInProgressHookで新しい Hook を作成しています。

function mountWorkInProgressHook(): Hook {
  const hook: 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;
}

とりあえず Hook の型をこんな感じです。

export type Hook = {
  memoizedState: any;
  baseState: any;
  baseQueue: Update<any, any> | null;
  queue: any;
  next: Hook | null;
};

それぞれのプロパティの意味は以下の通りです。

hook.memoizedState: メモリに保持されているローカルな状態。
hook.baseState: hook.baseQueue 内のすべてのアップデートオブジェクトがマージされた後の状態。
hook.baseQueue: 現在のレンダリング優先度よりも高いものだけを含む、アップデートオブジェクトの循環的なチェーン。
hook.queue: 優先度の高いすべてのアップデートオブジェクトを含む、アップデートオブジェクトの循環的なチェーン。
hook.next: 次のポインタ、チェーンの次のフックを指します。

引用元: Hook 原理(概览)

workInProgressHookは、進行中の Fiber に追加される新しい Hook のリストです。
ざっくりですが、Fiber とは コンポーネントに対応するデータを保持するインスタンスです。
各コンポーネント は必ずcurrnetworkInProgressの 2 つのインスタンスが存在します。
currentは、現在のコンポーネントの状態を保持しています。
workInProgressは、これから状態更新が反映される Fiber です。

# 最後に

とりあえず、Hook の作成までコードを追っていきました。
Fiber や Lane など新しい概念が出てきましたが、今回は深くは触れませんでした。
次回は、setState 内の実装を見ていこうと思います。