Reactのアーキテクチャを知る useState編1
こんにちは、noyanです。React内部実装アドベントカレンダーの1日目です。
この記事ではReactのuseStateフックの内部実装を読み解いていきます。
概要
Reactを使っている人のほとんどがuseStateを使ったことがあるでしょう。一方で内部実装を知っているひとはかなり限られるように思います。
このシリーズでは、useState
のコードリーディングを行います。useState
HookのAPIは便利で直感的ですが、その実装は複雑で難解です。useState
を我々が呼び出す時、裏でどのような処理が行われているのでしょうか。useState
編では関連する内部処理を概覧し、メンタルモデルを構築することを試みます。
「でも役にたたないでしょ?」。たしかにuseState
を実際に使う役には立たないかもしれません。ですが、Reactの内部実装のなかで比較的閉じたスコープのAPIを読解できるようになれば、あなたは次から自力でコードを読む手がかりを得るでしょう。Server ComponentsやConcurrent Renderingについて他人の寸評で耳学問するより、コードを知って判断するほうがずっとクールだと思いませんか。
コードリーディング
useState編1ではconst [count, setCount] = useState(0)
というありふれたコードを題材に、useState
がマウント時にどう処理されているか理解します。
useState: エントリーポイント
さて、useStateを呼んでみましょう。
const [count, setCount] = useState(0)
useState
関数はこのように定義されています。
export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
dispatcherのuseState
メソッドを使用しているようです。
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; return ((dispatcher: any): Dispatcher); }
resolveDispatcher
はほとんどなにもしていません。ReactCurrentDispatcher
に直接さわらないことで、実装修正の影響スコープを狭めるという見方が正しそうです。
ここまで見て、useState(0)
はReactCurrentDispatcher.useState(0)
を返していることがわかりました。
renderWithHooks
次にReactCurrentDispatcher
に代入を行う部分を見る必要があります。その場所とはrenderWithHooks
です。実際のコードは多少長いですが、概略的には次の処理をしています。
export function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRanderLane):any { // Dispatcherを付与 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // 更新がある限り計算を続ける let children do {children = Component(props)} while (hasUpdate) return children }
Dispatcherを割り当てた後、コンポーネントをレンダリングしています。たとえば次のGreet
コンポーネントがあるとします。
const Greet= ({name}) => (<div>Hello, {name}!</div>) // or // const Greet = ({ name })=>React.createElement("div", null, "Hello, ", name, "!")
renderWithHooks
にGreet
コンポーネントが与えられるとGreet({name})
を計算して、その計算結果をchildren
として返しているわけです。実際にレンダリングの計算をしている部分という感じがします。
さて、上の4行目でReactCurrentDIspatcher
に HooksDispatcherOnMount
かHooksDispatcherOnUpdate
を割り当てています。
代入されている変数の接尾語には、それぞれonMountとonUpdateとあります。我々はいまuseState
のマウント時の挙動に興味があるので、onMountを見ることにします。更新時の挙動を調べるときには、またここから始めることになるでしょう。
HooksDispatcherOnMount
このDispatcherはオブジェクトで、キーに各Hooks、値にmount時に呼ぶべき関数が定義されています。
これまでuseState
はReactCurrentDispatcher.useState(initialState)
を返す関数だとわかっているので、useState(0)
はHooksDispatcherOnMount.useState(0)
、つまりmountState(0)
を実行していることになります。
const HooksDispatcherOnMount: Dispatcher = { readContext, // ... useState: mountState, useReducer: mountReducer, useRef: mountRef, // ... };
mountState
まず戻り値だけ見てみましょう。mountState
は[hook.memoizedState, dispatch]
を返します。
function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { // ... return [hook.memoizedState, dispatch]; }
つまり、useState
はmountState
の実行結果を戻り値としているわけです。マウント時には実質的に次のコードが等価であることがわかりました。
const [count, setCount] = useState(0); const [count, setCount] = mountState(0)
おわり
「さて、コードを詳しくみていこう!」といきたいのですが、これには少し道具立てが必要です。また明日トライしてみることにしましょう。気になる人のためにmountState
のコードを貼っておきます。
function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // $FlowFixMe: Flow doesn't like mixed types initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue: UpdateQueue<S, BasicStateAction<S>> = { pending: null, interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }; hook.queue = queue; const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchSetState.bind( null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch]; }