eslint-plugin-reactのno-exhaustive-depsは何故propsのプロパティの関数を使う際にpropsも依存に要求するのか

TL;DR

  • オブジェクトのpropetyの関数をコールする際にthisが渡されるため。
  • propsは常に分割代入しておくのがよいとされる(react/destructuring-assingmentでESLintに設定可能)。

背景

eslint-plugin-reactのno-exhaustive-depsはpropsのpropertyの関数を使う際に下記のようにprops自身も要求します (正確にはpropsに限らずオブジェクトのpropertyを関数として使用するとオブジェクト自身も要求します)。

useEffect(
  () => props.updateNumber(5),
  [props.updateNumber],  // React Hook useEffect has a missing dependency: 'props'.
);

props内のプロパティとしてはprops.updateNumberしか使用していないのに何故 propsを要求するのでしょうか?

理由

no-exhaustive-depsが追加された時にフィードバックを募るIssueで同様の質問がありました。

Not sure if this is intentional or not: When you call a function from props, the linter suggests adding the entire props object as a dependency.

和訳

意図的なのかわからないのですが、propsから関数を読んだ時、リンターが依存としてpropオブジェクト全体を追加することを提案してきます。

それに対する回答は次のとおりです。

This is because technically props.foo() passes props itself as this to foo call. So foo might implicitly depend on props. We'll need a better message for this case though. The best practice is always destructuring.

和訳

それは厳密に言うとprops.foo()thisとしてprops自身をfoo呼び出しに渡すためです。fooは暗黙的にpropsに依存している可能性があります。このケースについてより良いメッセージが必要ですね。ベストプラクティスは常に分割代入することです。

つまり、オブジェクトのプロパティを関数として呼び出すとthisとしてオブジェクト自身を使用するため、オブジェクトにも依存してしまうのでexhaustive-depsはオブジェクトを依存のリストに追加することを提案します。

挙動の確認

FirefoxのConsoleでthisへの依存について動作確認してみます。

>> function func() { return this.a; }
<< undefined
>> func(); // 関数をそのまま呼ぶとthisはグローバルオブジェクトであるウィンドウオブジェクトで、this.aはwindow.aでありundefinedです
<< undefined
>> const x = { a: 'aaa', f: func };
<< undefined
>> x.f(); // xのプロパティとして呼ぶとthisはxになるので、this.aはx.aであり'aaa'です。
<< "aaa"
>> const { f } = x;
<< undefined
>> f(); // 分割代入をして呼び出すと、thisはウィンドウオブジェクトになりthis.aはundefinedになります。
<< undefined

次のことがわかります。

  • オブジェクトのプロパティを関数として呼び出すとオブジェクトの状態に依存して結果が変わる場合がある
  • 分割代入をしてプロパティとしての呼び出しでないようにするとthisによるオブジェクトへのアクセスはできなくなる

対処法

props内の関数を使用したいけどprops自身をdepsに加えたくない場合はどのようにすればよいかというと、理由として引用したコメントに書かれているとおり、常にpropsを分割代入するようにすればよいです。

常にpropsを分割代入することを推奨するためルールも存在するので、設定しておくとスタイルを統一できます。