
目次
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」のルールを設定して、違反している場合は以下のようになります。

こういうミスはコード量が多い場合は目視ですべて見つけるのはほぼ不可能なので、必ずESLintで設定して検出できるようにしたほうが良いです。
VS Codeの設定のESLintも有効にする
VS Codeの設定(Settings)でESLintで検索するとESLintの項目が表示されます。
VS Codeでは設定(Settings)の「ESlint: Enable」と「Eslint › Format: Enable」が有効になっていないと、VS Codeの画面上でコードがルールに違反していても、警告表示や自動修正が行われないので、VS CodeのESLintのこれらの設定は必ず有効にしてください。

これらを有効にしたのに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が含まれていることが確認できます。

package.jsonのscriptsに「"lint": "eslint ."」があるので「npm run lint」コマンドも使えます。
npm run lint
試しにApp.tsxにuseEffectに依存配列 [foo] の以下のコードを追加して「npm run lint」を実行してみてください。
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-hooksとeslint-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',」のルールを追加してください。
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] のように統一されているか確認するためのルールです。

試しに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] のように統一されているか確認する。
// OK ✅
const [count, setCount] = useState(0)
// NG ❌
const [data, updateData] = useState(0)
react/checked-requires-onchange-or-readonly
入力要素のチェック済みプロパティにonChangeまたはreadonly属性を強制する。
{/* OK ✅ */}
<input type="checkbox" onChange={() => console.log('checked')} checked />
{/* NG ❌ */}
<input type="checkbox" checked />
react/destructuring-assignment
propsの分割代入を強制する。
// 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で命名する。
// OK ✅
const Foo = memo(() => {
return <h1>Foo</h1>
})
Foo.displayName = 'Foo'
// NG ❌
const Foo = memo(() => {
return <h1>Foo</h1>
})
コンポーネント名が無名(Anonymous)かどうかはReact Developer Toolsで確認できます。

react/forbid-component-props
コンポーネントにstyleやclassNameなどの複雑なプロパティを渡さない。
// OK ✅
<Foo color="red" myClass="example" />
// NG ❌
<Foo style={{color: 'red'}} className="example" />
react/forward-ref-uses-ref
forwardRefでrefが渡されていないときに警告する。
// 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
コンポーネントをアロー関数に統一する。
// OK ✅
const App = () => {
return <h1>App</h1>
}
// NG ❌
function App() {
return <h1>App</h1>
}
react/iframe-missing-sandbox
iframeタグにsandbox属性がない場合は警告する。
// 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値の渡し方を統一する。
// OK ✅
<Child isVisible />
<Child isVisible={false} />
<Child isVisible={new Date().getMonth() === 11} />
// NG ❌
<Foo isVisible={true} />
react/jsx-child-element-spacing
JSXのインライン要素で途中の改行のみを禁止するルール。
// OK ✅
<div>
<b>This text</b>
{' '}
is bold
</div>
// NG ❌
<div>
<b>This text</b>
is bold
</div>
react/jsx-closing-bracket-location
JSXの閉じ括弧の位置を強制する。
// 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の終了タグの位置を強制する。
// OK ✅
<Hello>World</Hello>
<Hello>
World
</Hello>
// NG ❌
<Hello>
World</Hello>
react/jsx-curly-brace-presence
JSXの中括弧を強制したり、不要な中括弧を禁止する。
// OK ✅
<Hello>Hello world</Hello>
<Hello attr="foo" />
// NG ❌
<Hello>{'Hello world'}</Hello>
<Hello attr={'foo'} />
react/jsx-curly-spacing
中括弧内の値の間にスペースを入れるか指定する。
// OK ✅
<Hello name={ firstname } />
<Hello name={ firstname} />
<Hello name={firstname } />
// NG ❌
<Hello name={firstname} />
react/jsx-filename-extension
コンポーネントの拡張子を指定する。
// OK ✅
// filename: MyComponent.tsx
function MyComponent() {
return <div />;
}
// NG ❌
// filename: MyComponent.jsx
function MyComponent() {
return <div />;
}
デフォルトの設定だと拡張子が「.jsx」のみになっているので、React + TypeScriptの場合は以下のように設定が必要です。
rules: {
'react/jsx-filename-extension': ['error', { 'extensions': ['.ts', '.tsx'] }],
}
react/jsx-fragments
React.Fragment (<>...</>)を省略せずに使用するのを禁止する。
// OK ✅
<>
<Hello />
</>
// NG ❌
<React.Fragment>
<Hello />
</React.Fragment>
jsx-indent
JSXのインデントを指定する。
// 'react/jsx-indent': ['error', 2] (半角スペース2つの場合)
// OK ✅
<>
<Hello />
</>
// NG ❌
<>
<Hello />
</>
react/jsx-indent-props
JSXのpropsのインデントを指定する。
// 'react/jsx-indent-props': ['error', 2] (半角スペース2つの場合)
// OK ✅
<Hello
foo="bar"
/>
// NG ❌
<Hello
foo="bar"
/>
react/jsx-no-bind
関数にbind()を使用したり、コンポーネントで関数を宣言して、関数をpropとして渡すことを禁止する。
レンダリングごとに新しい関数生成されることを防ぐルールなので、SHIFTグループのようにコーディングルールでuseCallbackが必須なら必要。
// 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が必要な場合に警告する。
// 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のテキストノードとして /* */ や // の挿入を禁止する。
// 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の値として、安定しない値(オブジェクトなど)が使われるのを防ぐ。
// 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
// 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の重複を検出する。
// OK ✅
<Hello firstName="John" lastName="Smith" />
// NG ❌
<Hello name="John" name="Smith" />
react/jsx-no-leaked-render
リーク値(0やNaNなど)がレンダリングされないようにする。
Reactでは、0やNaNのような予期しない値をレンダリングすることがあります。
// 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を禁止する。
// 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
不要なフラグメント(<>...</>)の使用を禁止する。
// OK ✅
<Foo />
// NG ❌
<><Foo /></>
react/jsx-pascal-case
コンポーネントをパスカルケースに統一する。
// OK ✅
<HelloWorld />
// NG ❌
<Hello_World />
react/jsx-props-no-multi-spaces
コンポーネントのすべての属性の間や前の半角スペースを複数存在することを禁止する。
// OK ✅
<Hello firstName="John" lastName="Smith" />
// NG ❌
<Hello firstName="John" lastName="Smith" />
react/jsx-props-no-spread-multi
propsのスプレッド構文の重複を検出する。
// OK ✅
<Hello {...props} {...props2} />
// NG ❌
<Hello {...props} {...props} />
react/jsx-space-before-closing
JSX要素の閉じ括弧の前に1つ以上の空白があるかどうかをチェックします。
// OK ✅
<Hello />
<Hello firstName="John" />
// NG ❌
<Hello/>
<Hello firstName="John"/>
react/jsx-tag-spacing
JSX構文要素の内部と周囲の空白をチェックします。
// OK ✅
<App/>
<input/>
<Provider></Provider>
// NG ❌
<App/ >
<input/
>
<Provider>< /Provider>
react/no-children-prop
propにchildrenが使用されるのを禁止する。
// OK ✅
<Hello firstName="John" />
// NG ❌
<Hello children="John" />
react/no-danger
dangerouslySetInnerHTMLの使用を禁止する。
// OK ✅
const Hello = () => {
return <div><b>Hello</b></div>
}
// NG ❌
const Hello = () => {
return <div dangerouslySetInnerHTML={{ __html: '<b>Hello</b>' }}></div>
}
react/no-deprecated
非推奨のメソッドを使おうとすると警告を表示します。
// 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
コンポーネントのネスト化を禁止する。
// 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
子要素を持たないコンポーネントが余計な閉じタグを付けることを禁止する。
// OK ✅
<Hello name="John" />
// NG ❌
<Hello name="John"></Hello>
eslint.config.jsについて
以上のルールを記載したeslint.config.jsは以下の通り。
推奨ルール (reactPlugin.configs.flat.recommended) を使えばrulesに記載するルールの量を減らせますが、不要なルールも含まれてしまうので、全部記載してあります。
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',
},
},
)