t28.dev

React Hooks の deps がどのように評価されているか、React Hooks の実装を読んで知る

2021/11/21
Tech

React Hooks の deps ってどうやって評価されてるの?

  • React - Introducing Hooks (おさらい)
    • React Hooks によって、関数コンポーネント内で状態や副作用が表現できる。
    • Effect Hook (例えば useEffect) では、関数コンポーネントで副作用が発生する処理が実装出来る。
    • deps(第 2 引数) で 副作用が依存している値を指定することで、副作用処理の実行を制御出来る。

…わけですが、副作用が依存している deps が更新されたかどうかをどのように評価しているか知りたくなりました。

ReactFiberHooks

Reactexport している useEffectReactHooks.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.currentDispatcher オブジェクトを代入しているのは ReactFiberHooks っぽい。

ReactFiberHooks には 各 Dispatcher の実装もありました。

Dispatcher の名前から、React の描画のライフサイクルに従って、dispatcher の実装が切り替わることが分かります。

useEffect に話を戻して、各 Dispatcher が実装している useEffect の実装を確認します。

HooksDispatcherOnMount

const HooksDispatcherOnMount: Dispatcher = {
  // ...
  useEffect: mountEffect,
  // ...
};

mountEffect

Development ビルド用の あれ これ をした後、mountEffectImpl に処理を渡しています。

mountEffectImpl

mount は deps が変化する前の状態なので、deps の比較もしていませんね。これは React の使用者としての API の理解と一致してる。

HooksDispatcherOnUpdate, HooksDispatcherOnRerender

const HooksDispatcherOnUpdate: Dispatcher = {
  // ...
  useEffect: updateEffect,
  // ...
};

updateEffect

  • mountEffect と同様、Development ビルド用の あれこれ をして、updateEffectImpl に処理を渡しています。

updateEffectImpl

比較に使用している 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, === は(もちろん == も) 比較結果が違う(等価性の比較と同一性) 。

結論

deps の各要素は Object.is同一値 かどうかをチェックされる。