【ESLint】Flat Config に eslint-plugin-react-hooks を追加する

# はじめに
この記事では、ESLint の Flat Config に React Hooks 向けルールのプラグインである `eslint-plugin-react-hooks` を追加する手順について記載します。

https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks

https://react.dev/reference/rules/rules-of-hooks

# 開発環境
開発環境は以下の通りです。

- Windows 11
- VSCode
- TypeScript 5.2.2
- React 18.3.1
- Vite 5.3.1
- Node.js 20.15.0
- npm 10.8.1
- ESLint 8.57.0
- Prettier 3.3.2
- @eslint/js 9.6.0
- eslint-config-prettier 9.1.0
- eslint-plugin-jsx-a11y 6.9.0
- eslint-plugin-react 7.34.3
- eslint-plugin-react-hooks 4.6.2
- globals 15.8.0
- typescript-eslint 7.15.0

# 事前準備
プロジェクトに ESLint のインストール・設定をしておきます。
React / TypeScript 環境における詳細手順はこちら。

https://qiita.com/Yasushi-Mo/items/c2e259f8e2a86b79cb8a

Prettier を追加する手順はこちら。

https://qiita.com/Yasushi-Mo/items/27d324f601edebb63019

# `eslint-plugin-react-hooks` のインストール
`eslint-plugin-react-hooks` をインストールします。

```powershell
npm install eslint-plugin-react-hooks --save-dev
```

# 一括設定
今までの記事で扱った以下のプラグインと異なり、2024年7月時点で、`eslint-plugin-react-hooks` には Flat Config 向けの共有設定（`Shareable Configs`）が提供されていないようです。

- [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react)
- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)
- [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)

そのため、上記のプラグインとは、設定方法が異なります。

- `eslint-plugin-react-hooks` をインポート
- `pugins` に `"react-hooks": pluginReactHooks` を追加
- `rules` に `...pluginReactHooks.configs.recommended.rules` を追加

```eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
import pluginReactJSXRuntime from "eslint-plugin-react/configs/jsx-runtime.js";
import pluginReactHooks from "eslint-plugin-react-hooks"; // Add
import jsxA11y from "eslint-plugin-jsx-a11y";
import eslintConfigPrettier from "eslint-config-prettier";

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
    settings: {
      react: {
        version: "detect",
      },
    },
    plugins: {
      "react-hooks": pluginReactHooks, // Add
    },
    rules: {
      ...pluginReactHooks.configs.recommended.rules, // Add
    },
  },
  { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReactConfig,
  pluginReactJSXRuntime,
  jsxA11y.flatConfigs.recommended,
  eslintConfigPrettier,
];
```

# 動作確認
`useEffect` の `dependencies` がない状態で、ESLint を実行してみます。

```App.tsx
import { useEffect, useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    alert("Count: " + count);
  }, []); // no dependencies

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  );
}

export default App;

```

以下のワーニングが表示されます。

```powershell
11:6  warning  React Hook useEffect has a missing dependency:'count'. 
Either include it or remove the dependency array
react-hooks/exhaustive-deps

✖ 1 problem (0 errors, 1 warning)
```

`count` を追加するとワーニングが消えます。

```App.tsx
import { useEffect, useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    alert("Count: " + count);
  }, [count]); // Add

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  );
}

export default App;
```

# 個別設定
`eslint-plugin-react-hooks` の各ルールを個別に設定することもできます。
`rules` へ `...pluginReactHooks.configs.recommended.rules` の代わりに個別のルールを記載することで設定できます。

```eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
import pluginReactJSXRuntime from "eslint-plugin-react/configs/jsx-runtime.js";
import pluginReactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import eslintConfigPrettier from "eslint-config-prettier";

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
    settings: {
      react: {
        version: "detect",
      },
    },
    plugins: {
      "react-hooks": pluginReactHooks,
    },
    rules: {
      "react-hooks/exhaustive-deps": "error", // Add
    },
  },
  { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReactConfig,
  pluginReactJSXRuntime,
  jsxA11y.flatConfigs.recommended,
  eslintConfigPrettier,
];

```

# 参考
- [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks)

# 関連記事
https://qiita.com/Yasushi-Mo/items/c2e259f8e2a86b79cb8a

https://qiita.com/Yasushi-Mo/items/27d324f601edebb63019

https://qiita.com/Yasushi-Mo/items/4e328e712340b4449551