👔

ESLintからBiomeへの移行時に特に差分があったexhaustive-depsの挙動について

2024/09/15に公開

開発中のプロダクトで、静的解析ツールをESLintからBiomeへ移行しました。基本的にはBiomeが提供するmigrateコマンドで移行できますが、ツール間で差異があるルールについては修正が必要でした。特に修正量が多かった、Reactの依存配列に関するルールについて詳しく調査しました。

ESLint Biome
exhaustive-deps useExhaustiveDependencies

※ 2024/9/15時点での仕様です

結論として、Biomeのルールの方が依存配列をより厳密にチェックでき、チーム内のコード記述を統一できると感じました。

exhaustive-depsについて

Reactが公式に公開しているESLintプラグインのルールです。
https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks

useEffectなどのフックを定義する際の依存配列をLintしてくれます。

// 依存関係の不足
const [count, setCount] = useState(0);
useEffect(() => {
  console.log(count);
}, []); // React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array

ルール違反となるのは主に以下の場合です。

  • 依存関係の不足: フック内で使用している変数や関数が依存配列に含まれていない。
  • 不要な依存関係: フック内で使用していない変数や関数が依存配列に含まれている。
  • 再レンダリングごとに新しい参照が生成される値を依存関係に指定している: 例えば、毎回新しい関数やオブジェクトを生成している場合など。

このルールはBiomeにおいてuseExhaustiveDependenciesというルールで再現されています。
https://biomejs.dev/ja/linter/rules/use-exhaustive-dependencies/

Biomeのルールに移行した際に、エラーとして指摘されるようになった差分をいくつか紹介します。

差分1: 安定な値や関数を依存配列に含めるとエラーになる

useStatesetState関数など、再レンダー間で値や参照が変化しないことが保証されている値や関数は、依存配列に含める必要はありません。含めても結果は変わらないのですが、Biomeのルールではこれをエラーとして指摘します。

const [count, setCount] = useState(0);
const increment = useCallback(() => {
  setCount((count) => count + 1);
} , [setCount]); // This hook specifies more dependencies than necessary: setCount

このエラー判定が煩わしいという意見もありますが、レビュー時に依存配列を精査する際のコミュニケーションコストを減らすことができます。特に、GitHub CopilotなどのAIアシスト時に意図せず含まれることがあるため、静的解析で記述を統一できるのは利点と考えています。

差分2: useEffectの過剰な依存関係

ESLintでも、依存配列にフック内で使用していない値を指定するとエラーになりますが、useEffectではエラーになりません。開発者が意図的に特定のタイミングで処理を実行したい場合があり、過剰な依存関係が必ずしも問題ではないからです。

const [count, setCount] = useState(0);
useEffect(() => {
  console.log("test");
}, [count]); // ESLintではエラーにならない

一方、Biomeの場合は過不足を厳密にチェックし、エラーになります。

const [count, setCount] = useState(0);
useEffect(() => {
  console.log("test");
}, [count]); // This hook specifies more dependencies than necessary: count

この差分については、両方に一長一短がありますが、過剰な依存関係を定義する際にignoreコメントを書くことで、意図を明示的に示すことができます。

const [count, setCount] = useState(0);
// biome-ignore lint/correctness/useExhaustiveDependencies: xxの仕様のためcountの更新時にも実行する
useEffect(() => {
  console.log("useEffect");
}, [count]);

まとめ

今回は、Biomeへの移行時に特に差分が大きかったルールについて紹介しました。上記で述べた差分以外にも細かな違いがあり、最終的に40ファイルほどを目視で修正しました。

Biomeは、ESLintでは存在したいくつかのルールが欠けているものの、「とにかく速い!」「これまで静的解析でこれほど待っていたことに気づいた」など、チーム内で好評でした。先日のv1.9.0のリリースでついにCSSにも対応したため、Prettierからも移行していく予定です💪!

https://biomejs.dev/blog/biome-v1-9/

Discussion