本記事は、React の内部実装を理解するための学習ログです。
前回の概要編に続き、今回は mountState / updateState に入る前の共通処理を読んでいきます。
はじめに
概要編では、useState の大まかな仕組みを整理しました。
useState が動くまでには、実は useState が呼ばれる前に React 側の準備処理があります。
実際の処理の流れを大きく示すと次のようになります。
【React 内部】
renderWithHooks() が呼ばれる ← React がコンポーネントを実行する前の準備
↓
Dispatcher をセット ← 今回はここ
↓
コンポーネント関数を実行
↓
【コンポーネント内】
useState(10) が呼ばれる
↓
① Dispatcher の解決 ← 今回はここ
↓
② mount か update かの振り分け ← 今回はここ
↓
③ mountState / updateState の実行
↓
④ Hook ノードの生成・Fiber への紐付け
↓
⑤ queue の初期化
↓
⑥ [state, dispatch] を返す
【React 内部】
finishRenderingHooks() ← Dispatcher をリセット
今回の記事では、renderWithHooks による Dispatcher のセット(準備処理)と、useState 内部の①②を読んでいきます。
具体的には「同じ useState がなぜ初回と更新で別の動きをするのか」という仕組みの部分です。
③以降は次回以降の記事で扱います。
useState を呼ぶと最初に何が起きるか
まず、useState の実装を見てみましょう。
// packages/react/src/ReactHooks.js
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
驚くほど薄いです。
useState 自体はほとんど何もしておらず、Dispatcher というオブジェクトに丸投げしています。
ポイント:
useStateは薄いラッパーに過ぎない。実体は Dispatcher 側にある。
Dispatcher とは何か
resolveDispatcher() の実装を見てみます。
// packages/react/src/ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
"Invalid hook call. Hooks can only be called inside of the body of a function component...",
);
}
}
return dispatcher;
}
ReactSharedInternals.H を返しているだけです。
ReactSharedInternals とは何か?
import している箇所を見ると、shared/ReactSharedInternals からインポートしています。
中身を確認すると、
import * as React from "react";
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE は、React が内部処理のために使うグローバルな共有オブジェクトです。
名前が非常に長いのは意図的で、「外部から触るな」というメッセージをそのまま名前にしています。
このオブジェクトを ReactSharedInternals という短い名前で export して、React 内部の様々なファイルから参照しやすくしています。
この H が 「今どのモードで Hook を実行すべきか」を表す Dispatcher です。
Dispatcher はオブジェクトで、useState や useEffect などの Hook の実装が入っています。
Dispatcher(イメージ)
useState: mountState または updateState
useEffect: mountEffect または updateEffect
...
重要なのは、同じ useState でも初回と2回目以降では別の関数が呼ばれるという点です。
【初回レンダー】 【再レンダー】
useState(10) useState(10)
↓ ↓
resolveDispatcher() resolveDispatcher()
↓ ↓
H = HooksDispatcherOnMount H = HooksDispatcherOnUpdate
↓ ↓
dispatcher.useState dispatcher.useState
= mountState(10) = updateState(10)
Dispatcher はいつ・どうセットされるか
ReactSharedInternals.H は renderWithHooks の中でセットされます。
renderWithHooks はコンポーネントを実行する前に呼ばれる関数で、
ここで「このコンポーネントは初回か更新か」を判断し、適切な Dispatcher をセットします。
// packages/react-reconciler/src/ReactFiberHooks.js(一部抜粋)
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes,
) {
// ...
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 初回
: HooksDispatcherOnUpdate; // 更新
// コンポーネント関数を実行(ここで useState が呼ばれる)
let children = Component(props, secondArg);
// ...
finishRenderingHooks(current, workInProgress, Component);
return children;
}
判定条件は current === null || current.memoizedState === null です。
次のセクションでこの条件の意味を見ていきます。
current.memoizedState とは何か
current は「前回レンダー済みの Fiber」です。
その memoizedState は、その Fiber に紐づく Hook リンクリストの先頭ポインタです。
Fiber (current)
└─ memoizedState
↓
Hook1 (useState)
↓ next
Hook2 (useState)
↓ next
Hook3 (useEffect)
つまり判定条件の意味はこうです。
current === null
→ Fiberがそもそも存在しない(初めてマウントされるコンポーネント)
→ mount
current.memoizedState === null
→ Fiberは存在するが、Hookをひとつも持っていない
→ mount
上記以外
→ 前回レンダー済みのHookが存在する
→ update
レンダーが終わったら Dispatcher はリセットされる
コンポーネントの実行が終わると、finishRenderingHooks が呼ばれます。
function finishRenderingHooks(current, workInProgress, Component) {
// ...
ReactSharedInternals.H = ContextOnlyDispatcher;
// ...
}
H が ContextOnlyDispatcher に上書きされます。
この Dispatcher は、Hook を呼ぶと必ずエラーを投げる実装になっています。
ContextOnlyDispatcher
useState: throwInvalidHookError // ← 呼ぶとエラー
useEffect: throwInvalidHookError
...
これが「コンポーネントの外で useState を呼ぶと Invalid hook call になる」理由です。
【レンダー中】
H = HooksDispatcherOnMount / HooksDispatcherOnUpdate
→ useState が正常に動く
【レンダー外】
H = ContextOnlyDispatcher
→ useState を呼ぶとエラー
ポイント:Dispatcher は render フェーズ中だけ有効で、終わったら即リセットされる。
まとめ:共通処理の全体像
今回読んだ処理の流れをまとめます。
【React 内部】
renderWithHooks() が呼ばれる
↓
current の状態を確認して Dispatcher をセット
current === null || current.memoizedState === null
→ HooksDispatcherOnMount (初回)
→ HooksDispatcherOnUpdate (更新)
↓
コンポーネント関数を実行
【コンポーネント内】
useState(10) が呼ばれる
↓
① Dispatcher の解決
resolveDispatcher() → ReactSharedInternals.H を取得
↓
② mount か update かの振り分け
dispatcher.useState(10) を呼ぶ
→ mountState(10) (初回)
→ updateState(10) (更新)
↓
③ mountState / updateState の実行 ← 次回以降
↓
④ Hook ノードの生成・Fiber への紐付け ← 次回以降
↓
⑤ queue の初期化 ← 次回以降
↓
⑥ [state, dispatch] を返す ← 次回以降
【React 内部】
finishRenderingHooks()
↓
H = ContextOnlyDispatcher(リセット)
次の記事では
次回は mountState の中身を読んでいきます。
初回レンダーで useState(10) が呼ばれたとき、
- Hook ノードがどう生成されるか
- Fiber にどう紐づけられるか
- リンクリストがどう構築されるか
を mountWorkInProgressHook() のソースコードを追いながら見ていきます。