VS Code, Vite, Reactの環境にeslint-plugin-reactを設定する方法と推奨ルール

eslint-plugin-reactとは

eslint-plugin-reactとはReactのコード品質を高めるためのESLintプラグインです。

Reactのベストプラクティスに基づいたルールを提供し、Reactのコードが正しいかを検証するのに役立ちます。

VS Code + Vite + Reactの環境だとESLintが最初から入っていますが、eslint-plugin-reactを使用する場合はインストールして、eslint.config.jsにルールの追加が必要です。

ルールに違反している箇所があればリアルタイムに赤線で警告が表示されてすぐにわかるようになります。

例えば、useStateの命名が [value, setValue] のように統一されているか確認するための「react/hook-use-state」のルールを設定して、違反している場合は以下のようになります。

VS CodeのESLintの警告表示

こういうミスはコード量が多い場合は目視ですべて見つけるのはほぼ不可能なので、必ずESLintで設定して検出できるようにしたほうが良いです。

VS Codeの設定のESLintも有効にする

VS Codeの設定(Settings)でESLintで検索するとESLintの項目が表示されます。

VS Codeでは設定(Settings)の「ESlint: Enable」と「Eslint › Format: Enable」が有効になっていないと、VS Codeの画面上でコードがルールに違反していても、警告表示や自動修正が行われないので、VS CodeのESLintのこれらの設定は必ず有効にしてください。

VS Code Eslint Format: Enable

これらを有効にしたのにVS Codeのコード上でルールの警告が表示されない場合は、VS Codeのコマンドパレットから「Developer: Reload Window」を選択して、VS Codeを再起動してください。

使用しているパソコンのスペックが低かったり、VS Codeにインストールしている拡張機能が多すぎる場合は、ESLintの警告が表示されるまで時間がかかります。

もしコード内にルール違反があるのに「npm run lint」コマンドを実行しても警告が出ない場合は、ルールの設定が間違っています。

VS Code + Vite + Reactの環境構築と設定方法

まず、Vite + Reactの環境を以下のコマンドで作成してください。

npm create vite@latest my-react-eslint -- --template react-swc-ts

my-react-eslintのプロジェクトフォルダができたら、以下のコマンドで起動できます。

cd my-react-eslint
npm install
npm run dev

次に「cd my-react-eslint」で移動した状態で、「code my-react-eslint」のコマンドでプロジェクトフォルダをVS Codeで開いてください。(codeコマンドが使えない場合はVS Codeで直接開く)

ESLintの設定ファイルのeslint.config.jsが含まれていることが確認できます。

VS CodeのESLintのeslint.config.js

package.jsonのscriptsに「"lint": "eslint ."」があるので「npm run lint」コマンドも使えます。

npm run lint

試しにApp.tsxにuseEffectに依存配列 [foo] の以下のコードを追加して「npm run lint」を実行してみてください。

TSX
useEffect(() => {
  console.log(foo)
}, [foo])

すると、依存配列 [foo] の部分で警告が表示されます。

$ npm run lint

> my-react-eslint@0.0.0 lint
> eslint .

  11:6  warning  React Hook useEffect has an unnecessary dependency: 'foo'. Either exclude it or remove the dependency array. Outer scope values like 'foo' aren't valid dependencies because mutating them doesn't re-render the component  react-hooks/exhaustive-deps

 1 problem (0 errors, 1 warning)

Vite + Reactの環境にはeslint-plugin-react-hookseslint-plugin-react-refreshがあらかじめインストールされています。

この2つだけで設定できるReactのルールは少ないので、eslint-plugin-reactをインストールしてルールを追加することをオススメします。

eslint-plugin-reactの追加方法

まず、npm i -D eslint-plugin-reactでインストールします。

npm i -D eslint-plugin-react

追加したら、eslint.config.jsでインポートして、pluginsにreactを追記してください。

警告の確認のためにrulesに「'react/hook-use-state': 'error',」のルールを追加してください。

eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import react from 'eslint-plugin-react'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'react/hook-use-state': 'error',
    },
  },
)

react/hook-use-stateは「eslint-plugin-reactとは」で説明したuseStateの命名が [value, setValue] のように統一されているか確認するためのルールです。

VS CodeのESLintの警告表示

試しにApp.tsxの [count, setCount] を [count, updateCount] に変えてみてください。

コードのルール違反を検知して「useState call is not destructured into value + setter pair」という警告が表示されます。

eslint-plugin-reactの推奨ルールは使わない方が良い

eslint-plugin-reactにはrecommended (推奨) のルールがいくつか存在します。

しかし、recommendedのルールの中には適用しないほうが良いものも含まれているため、plugin:react/recommendedを設定して一括で推奨ルールを使用するのは避けた方が良いです。

例えば、jsx-no-target-blankというルールが存在しますが、target="_blank" で rel="noreferrer" を強制するルールとなっていますが、rel="noreferrer" はWeb解析やSEOの面でマイナスになるので、jsx-no-target-blankを設定している人は少ないです。

eslint-plugin-reactのiwb.jp追加推奨ルール

eslint-plugin-reactには全部で100個以上のルールが存在します。

たくさんルールがありますが、現在のReactは関数コンポーネントが主流で、クラスコンポーネントはほとんど使われないので、クラスコンポーネントのルールなどを除けば実際に使えるルールははんぶん以下になります。

その中でiwb.jpが追加を推奨するルールは以下の通りです。

react/hook-use-state

useStateの命名が [value, setValue] のように統一されているか確認する。

TSX
// OK ✅
const [count, setCount] = useState(0)

// NG ❌
const [data, updateData] = useState(0)

react/checked-requires-onchange-or-readonly

入力要素のチェック済みプロパティにonChangeまたはreadonly属性を強制する。

TSX
{/* OK ✅ */}
<input type="checkbox" onChange={() => console.log('checked')} checked />

{/* NG ❌ */}
<input type="checkbox" checked />

react/destructuring-assignment

propsの分割代入を強制する。

TSX
// OK ✅
const Foo = ({id, name}: Props) => {
  return (
    <div>{id}: {name}</div>
  )
}

// NG ❌
const Bar = (props: Props) => {
  return (
    <div>{props.id}: {props.name}</div>
  )
}

react/display-name

メモ化などによりコンポーネント名が無名(Anonymous)になったらdisplayNameで命名する。

TSX
// OK ✅
const Foo = memo(() => {
  return <h1>Foo</h1>
})
Foo.displayName = 'Foo'

// NG ❌
const Foo = memo(() => {
  return <h1>Foo</h1>
})

コンポーネント名が無名(Anonymous)かどうかはReact Developer Toolsで確認できます。

React Developer Tools Component Anonymous

react/forbid-component-props

コンポーネントにstyleやclassNameなどの複雑なプロパティを渡さない。

TSX
// OK ✅
<Foo color="red" myClass="example" />

// NG ❌
<Foo style={{color: 'red'}} className="example" />

react/forward-ref-uses-ref

forwardRefでrefが渡されていないときに警告する。

TSX
// OK ✅
const Foo = forwardRef((_props, ref) => {
  return <input ref={ref} type="text" />
})

// NG ❌
const Foo = forwardRef((_props) => {
  return <input type="text" />
})

react/forward-ref-uses-ref

コンポーネントをアロー関数に統一する。

TSX
// OK ✅
const App = () => {
  return <h1>App</h1>
}

// NG ❌
function App() {
  return <h1>App</h1>
}

react/iframe-missing-sandbox

iframeタグにsandbox属性がない場合は警告する。

TSX
// OK ✅
const App = () => {
  return (
    <iframe
      src="https://iwb.jp/s/alert-hello/"
      // ポップアップ(alertなど)の生成を制限
      sandbox="allow-scripts"
    ></iframe>
  )
}

// NG ❌
const App = () => {
  return (
    <iframe
      src="https://iwb.jp/s/alert-hello/"
    ></iframe>
  )
}

react/jsx-boolean-value

JSXのboolean値の渡し方を統一する。

TSX
// OK ✅
<Child isVisible />
<Child isVisible={false} />
<Child isVisible={new Date().getMonth() === 11} />

// NG ❌
<Foo isVisible={true} />

react/jsx-child-element-spacing

JSXのインライン要素で途中の改行のみを禁止するルール。

TSX
// OK ✅
<div>
  <b>This text</b>
  {' '}
  is bold
</div>

// NG ❌
<div>
  <b>This text</b>
  is bold
</div>

react/jsx-closing-bracket-location

JSXの閉じ括弧の位置を強制する。

TSX
// OK ✅
<Hello lastName="Smith" firstName="John" />

<Hello
  lastName="Smith"
  firstName="John"
/>

// NG ❌
<Hello
  lastName="Smith"
  firstName="John" />

<Hello
  lastName="Smith"
  firstName="John"
  />

react/jsx-closing-tag-location

JSXの終了タグの位置を強制する。

TSX
// OK ✅
<Hello>World</Hello>
<Hello>
  World
</Hello>

// NG ❌
<Hello>
  World</Hello>

react/jsx-curly-brace-presence

JSXの中括弧を強制したり、不要な中括弧を禁止する。

TSX
// OK ✅
<Hello>Hello world</Hello>
<Hello attr="foo" />

// NG ❌
<Hello>{'Hello world'}</Hello>
<Hello attr={'foo'} />

react/jsx-curly-spacing

中括弧内の値の間にスペースを入れるか指定する。

TSX
// OK ✅
<Hello name={ firstname } />
<Hello name={ firstname} />
<Hello name={firstname } />

// NG ❌
<Hello name={firstname} />

react/jsx-filename-extension

コンポーネントの拡張子を指定する。

TSX
// OK ✅
// filename: MyComponent.tsx
function MyComponent() {
  return <div />;
}

// NG ❌
// filename: MyComponent.jsx
function MyComponent() {
  return <div />;
}

デフォルトの設定だと拡張子が「.jsx」のみになっているので、React + TypeScriptの場合は以下のように設定が必要です。

eslint.config.js
rules: {
  'react/jsx-filename-extension': ['error', { 'extensions': ['.ts', '.tsx'] }],
}

react/jsx-fragments

React.Fragment (<>...</>)を省略せずに使用するのを禁止する。

TSX
// OK ✅
<>
  <Hello />
</>

// NG ❌
<React.Fragment>
  <Hello />
</React.Fragment>

jsx-indent

JSXのインデントを指定する。

TSX
// 'react/jsx-indent': ['error', 2] (半角スペース2つの場合)
// OK ✅
<>
  <Hello />
</>

// NG ❌
<>
    <Hello />
</>

react/jsx-indent-props

JSXのpropsのインデントを指定する。

TSX
// 'react/jsx-indent-props': ['error', 2] (半角スペース2つの場合)
// OK ✅
<Hello
  foo="bar"
/>

// NG ❌
<Hello
    foo="bar"
/>

react/jsx-no-bind

関数にbind()を使用したり、コンポーネントで関数を宣言して、関数をpropとして渡すことを禁止する。

レンダリングごとに新しい関数生成されることを防ぐルールなので、SHIFTグループのようにコーディングルールでuseCallbackが必須なら必要。

TSX
// OK ✅
const App = () => {
  const [count, setCount] = useState(0)
  const handleClick = useCallback(() => {
    setCount((count) => count + 1)
  }, [])

  return (
    <>
      <button onClick={handleClick}>{count}</button>
    </>
  )
}

// NG ❌
const App = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount((count) => count + 1)}>{count}</button>
    </>
  )
}

react/jsx-key

JSXでkeyが必要な場合に警告する。

TSX
// OK ✅
const App = () => {
  return Array.from([1, 2, 3], (x) => <Hello key={x}>{x}</Hello>)
}

// NG ❌
const App = () => {
  return Array.from([1, 2, 3], (x) => <Hello>{x}</Hello>)
}

react/jsx-no-comment-textnodes

JSXのテキストノードとして /* */ や // の挿入を禁止する。

TSX
// OK ✅
const Foo = () => {
  return <div>{/* empty */}</div>
}

// NG ❌
const Foo = () => {
  return <div>// empty</div>
}

const Bar = () => {
  return <div>/* empty */</div>
}

react/jsx-no-constructed-context-values

Context.Providerの値として、安定しない値(オブジェクトなど)が使われるのを防ぐ。

TSX
// OK ✅
import React, { createContext, useContext, useMemo } from 'react'

const MyContext = createContext({ foo: '' })
const MyProvider = ({ children }: { children: React.ReactNode }) => {
  const foo = useMemo(() => ({foo: 'bar'}), [])
  return (
    <MyContext.Provider value={foo}>
      {children}
    </MyContext.Provider>
  )
}

const UserComponent = () => {
  const { foo } = useContext(MyContext)
  return <div>{foo}</div>
}

const App = () => {
  return (
    <MyProvider>
      <UserComponent />
    </MyProvider>
  )
}

export default App
TSX
// NG ❌
import React, { createContext, useContext } from 'react'

const MyContext = createContext({ foo: '' })
const MyProvider = ({ children }: { children: React.ReactNode }) => {
  return (
    <MyContext.Provider value={{ foo: 'bar' }}>
      {children}
    </MyContext.Provider>
  )
}

const UserComponent = () => {
  const { foo } = useContext(MyContext)
  return <div>{foo}</div>
}

const App = () => {
  return (
    <MyProvider>
      <UserComponent />
    </MyProvider>
  )
}

export default App

react/jsx-no-duplicate-props

propsの重複を検出する。

TSX
// OK ✅
<Hello firstName="John" lastName="Smith" />

// NG ❌
<Hello name="John" name="Smith" />

react/jsx-no-leaked-render

リーク値(0やNaNなど)がレンダリングされないようにする。

Reactでは、0やNaNのような予期しない値をレンダリングすることがあります。

TSX
// OK ✅
const App = () => {
  const randomNum = Math.floor(Math.random() * 3)

  return (
    <>
      <p>randomNum: {randomNum}</p>
      {randomNum ? <Hello firstName="John" /> : null}
    </>
  )
}

// NG ❌
const App = () => {
  const randomNum = Math.floor(Math.random() * 3)

  return (
    <>
      <p>randomNum: {randomNum}</p>
      {/* randomNumが0のときに0が表示されてしまう */}
      {randomNum && <Hello firstName="John" />}
    </>
  )
}

react/jsx-no-script-url

href="javascript:" のようなURLを禁止する。

TSX
// OK ✅
const App = () => {
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault()
    alert(1)
  }

  return <a href="#" onClick={handleClick}>alert(1)</a>
}

export default App

// NG ❌
<a href="javascript: alert(1)">alert(1)</a>

react/jsx-no-useless-fragment

不要なフラグメント(<>...</>)の使用を禁止する。

TSX
// OK ✅
<Foo />

// NG ❌
<><Foo /></>

react/jsx-pascal-case

コンポーネントをパスカルケースに統一する。

TSX
// OK ✅
<HelloWorld />

// NG ❌
<Hello_World />

react/jsx-props-no-multi-spaces

コンポーネントのすべての属性の間や前の半角スペースを複数存在することを禁止する。

TSX
// OK ✅
<Hello firstName="John" lastName="Smith" />

// NG ❌
<Hello  firstName="John"  lastName="Smith" />

react/jsx-props-no-spread-multi

propsのスプレッド構文の重複を検出する。

TSX
// OK ✅
<Hello {...props} {...props2} />

// NG ❌
<Hello {...props} {...props} />

react/jsx-space-before-closing

JSX要素の閉じ括弧の前に1つ以上の空白があるかどうかをチェックします。

TSX
// OK ✅
<Hello />
<Hello firstName="John" />

// NG ❌
<Hello/>
<Hello firstName="John"/>

react/jsx-tag-spacing

JSX構文要素の内部と周囲の空白をチェックします。

TSX
// OK ✅
<App/>
<input/>
<Provider></Provider>

// NG ❌
<App/ >
<input/
>
<Provider>< /Provider>

react/no-children-prop

propにchildrenが使用されるのを禁止する。

TSX
// OK ✅
<Hello firstName="John" />

// NG ❌
<Hello children="John" />

react/no-danger

dangerouslySetInnerHTMLの使用を禁止する。

TSX
// OK ✅
const Hello = () => {
  return <div><b>Hello</b></div>
}

// NG ❌
const Hello = () => {
  return <div dangerouslySetInnerHTML={{ __html: '<b>Hello</b>' }}></div>
}

react/no-deprecated

非推奨のメソッドを使おうとすると警告を表示します。

TSX
// NG ❌
React.render(<MyComponent />, root)
React.unmountComponentAtNode(root)
React.findDOMNode(this.refs.foo)
React.renderToString(<MyComponent />)
React.renderToStaticMarkup(<MyComponent />)
React.createClass({ /* Class object */ })
const propTypes = {
  foo: PropTypes.bar,
};
React.DOM.div()

componentWillMount() { }
componentWillReceiveProps() { }
componentWillUpdate() { }

import { render } from 'react-dom';
ReactDOM.render(<div></div>, container)

import { hydrate } from 'react-dom';
ReactDOM.hydrate(<div></div>, container)

import {unmountComponentAtNode} from 'react-dom';
ReactDOM.unmountComponentAtNode(container)

import { renderToNodeStream } from 'react-dom/server';
ReactDOMServer.renderToNodeStream(element)

react/no-unstable-nested-components

コンポーネントのネスト化を禁止する。

TSX
// OK ✅
const NestedComponent = () => {
  return <div>example</div>
}

const Component = () => {
  return (
    <div>
      <NestedComponent />
    </div>
  )
}

// NG ❌
const Component = () => {
  const NestedComponent = () => {
    return <div>example</div>
  }

  return (
    <div>
      <NestedComponent />
    </div>
  )
}

react/self-closing-comp

子要素を持たないコンポーネントが余計な閉じタグを付けることを禁止する。

TSX
// OK ✅
<Hello name="John" />

// NG ❌
<Hello name="John"></Hello>

eslint.config.jsについて

以上のルールを記載したeslint.config.jsは以下の通り。

推奨ルール (reactPlugin.configs.flat.recommended) を使えばrulesに記載するルールの量を減らせますが、不要なルールも含まれてしまうので、全部記載してあります。

eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import react from 'eslint-plugin-react'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'quotes': ['error', 'single'],
      'semi': ['error', 'never'],
      'react/hook-use-state': 'error',
      'react/checked-requires-onchange-or-readonly': 'error',
      'react/default-props-match-prop-types': 'error',
      'react/destructuring-assignment': 'error',
      'react/display-name': 'error',
      'react/forbid-component-props': 'error',
      'react/forward-ref-uses-ref': 'error',
      'react/function-component-definition': [
        'error',
        {
          namedComponents: 'arrow-function',
          unnamedComponents: 'arrow-function',
        },
      ],
      'react/iframe-missing-sandbox': 'error',
      'react/jsx-boolean-value': 'error',
      'react/jsx-child-element-spacing': 'error',
      'react/jsx-closing-bracket-location': 'error',
      'react/jsx-closing-tag-location': 'error',
      'react/jsx-curly-brace-presence': 'error',
      'react/jsx-curly-spacing': 'error',
      'react/jsx-filename-extension': ['error', { 'extensions': ['.ts', '.tsx'] }],
      'react/jsx-fragments': 'error',
      'react/jsx-indent': ['error', 2],
      'react/jsx-indent-props': ['error', 2],
      'react/jsx-no-bind': 'error',
      'react/jsx-key': 'error',
      'react/jsx-no-comment-textnodes': 'error',
      'react/jsx-no-constructed-context-values': 'error',
      'react/jsx-no-duplicate-props': 'error',
      'react/jsx-no-leaked-render': 'error',
      'react/jsx-no-script-url': 'error',
      'react/jsx-no-useless-fragment': 'error',
      'react/jsx-pascal-case': 'error',
      'react/jsx-props-no-multi-spaces': 'error',
      'react/jsx-props-no-spread-multi': 'error',
      'react/jsx-tag-spacing': 'error',
      'react/no-children-prop': 'error',
      'react/no-danger': 'error',
      'react/no-deprecated': 'error',
      'react/no-unstable-nested-components': 'error',
      'react/self-closing-comp': 'error',
    },
  },
)