React Hooks の deps がどのように評価されているか、React Hooks の実装を読んで知る
- “React Hooks の deps にオブジェクトを渡す” の副産物
- React Hooks の deps がどのように評価されているか、実装を
適当に端折って追う。
React Hooks の deps ってどうやって評価されてるの?
- React - Introducing Hooks (おさらい)
- React Hooks によって、関数コンポーネント内で状態や副作用が表現できる。
- Effect Hook (例えば
useEffect
) では、関数コンポーネントで副作用が発生する処理が実装出来る。 - deps(第 2 引数) で 副作用が依存している値を指定することで、副作用処理の実行を制御出来る。
…わけですが、副作用が依存している deps が更新されたかどうかをどのように評価しているか知りたくなりました。
ReactFiberHooks
React が export している useEffect は ReactHooks.js で定義されています。
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
resolveDispatcher から 現在の dispatcher (ReactCurrentDispatcher) を取得して、実際の useEffect の処理を実行しているようです。 “current” が付く Dispatcher なので、Dispatcher は色々状態を持つようですね。
横着して grep した結果、 ReactCurrentDispatcher.current
に Dispatcher オブジェクトを代入しているのは ReactFiberHooks っぽい。
ReactFiberHooks には 各 Dispatcher の実装もありました。
- 基本的なものが 4 種類
- 開発環境のみ(Production ビルド のときはnull) 、使えるものが 7 種類
- HooksDispatcherOnRerenderInDEV
- HooksDispatcherOnMountInDEV
- HooksDispatcherOnMountWithHookTypesInDEV
- HooksDispatcherOnUpdateInDEV
- InvalidNestedHooksDispatcherOnRerenderInDEV
- InvalidNestedHooksDispatcherOnMountInDEV
- InvalidNestedHooksDispatcherOnUpdateInDEV
Dispatcher の名前から、React の描画のライフサイクルに従って、dispatcher の実装が切り替わることが分かります。
useEffect
に話を戻して、各 Dispatcher が実装している useEffect
の実装を確認します。
HooksDispatcherOnMount
const HooksDispatcherOnMount: Dispatcher = {
// ...
useEffect: mountEffect,
// ...
};
mountEffect
Development ビルド用の あれ これ をした後、mountEffectImpl
に処理を渡しています。
mountEffectImpl
- nextDeps を作って
- React Hooks 用の メモ化された状態オブジェクト に Effect オブジェクトを追加している。
mount は deps が変化する前の状態なので、deps の比較もしていませんね。これは React の使用者としての API の理解と一致してる。
HooksDispatcherOnUpdate, HooksDispatcherOnRerender
const HooksDispatcherOnUpdate: Dispatcher = {
// ...
useEffect: updateEffect,
// ...
};
updateEffect
mountEffect
と同様、Development ビルド用の あれこれ をして、updateEffectImpl
に処理を渡しています。
updateEffectImpl
- nextDeps と prevDeps を比較して
- 同じなら、return
- 異なるなら、
mountEffectImpl
と同様に メモ化された状態オブジェクト に Effect オブジェクトを追加
比較に使用している areHookInputsEqual が 副作用関数を実行する/実行しないを決定するロジックのようですね。
ContextOnlyDispatcher
const ContextOnlyDispatcher: Dispatcher = {
// ...
useEffect: throwInvalidHookError,
// ...
};
throwInvalidHookError
不正な React Hooks の呼び出しをお知らせする だけ。
Object.is
updateEffectImpl が実行している areHookInputsEqual は、Development ビルド用の あれ これ それ をした後に、 2 つの deps の 配列要素毎 に is() で値の比較をして、1 つでも異なるものがあれば deps がが更新されていると判断されるようです。
React が実装している is()
は Object.is
の polyfill のようなので、MDN を見ます。
Object.is() は 2 つの値が同一値であるかどうかを判定します。2 つの値が以下の規則の一つに当てはまる場合に同一となります。
- どちらも undefined
- どちらも null
- どちらも true かどちらも false
- どちらも同じ文字からなる同じ長さの文字列
- どちらも同じオブジェクト
- どちらも数で、どちらも +0、どちらも -0、どちらも NaN、あるいはどちらもゼロ以外で NaN でなく、同じ数値を持つ
余談: Object.is
, ===
は(もちろん ==
も) 比較結果が違う(等価性の比較と同一性) 。