React 開発で役立つ ESLint ルール使い方まとめ

React 開発では、コンポーネントの複雑化やチーム開発での一貫性確保など、様々な課題に直面します。ESLint は、これらの課題を自動的に検出し、コード品質を向上させる強力なツールです。しかし、React 特有の問題に対応するには、適切なルール設定が欠かせません。
この記事では、React 開発において実際に役立つ ESLint ルールを体系的にご紹介いたします。基本的な設定から高度な最適化まで、実践的なコード例とともに詳しく解説していきます。開発効率とコード品質の両方を向上させるための具体的な手法を、ぜひマスターしてください。
ESLint と React 開発の関係性
ESLint が React 開発にもたらす価値
ESLint は、JavaScript コードの品質とスタイルを自動的にチェックするリンティングツールです。React 開発において、ESLint は単なるコードフォーマッターを超えた重要な役割を果たします。
ESLint の主要な機能
typescript// ESLint が検出できる問題の例
import React, { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
// ESLint が検出: useEffect の依存関係配列が不適切
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId が依存関係に含まれていない
// ESLint が検出: 未使用の変数
const unusedVariable = 'この変数は使われていません';
// ESLint が検出: 不適切な条件分岐
if (user == null) {
// == ではなく === を推奨
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
React エコシステムでの ESLint の位置づけ
React 開発では、複数の ESLint プラグインとルールセットが連携して動作します。
主要な ESLint プラグインの関係
# | プラグイン名 | 主な役割 | 対象範囲 |
---|---|---|---|
1 | eslint-plugin-react | React 特有のルールを提供 | JSX、コンポーネント設計 |
2 | eslint-plugin-react-hooks | React Hooks の適切な使用を保証 | useState、useEffect など |
3 | eslint-plugin-jsx-a11y | アクセシビリティの問題を検出 | ARIA、セマンティック HTML |
4 | @typescript-eslint/eslint-plugin | TypeScript 対応の拡張ルール | 型安全性、TypeScript 固有機能 |
開発ワークフローでの ESLint 活用
json{
"scripts": {
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"lint:staged": "lint-staged",
"pre-commit": "yarn lint:staged"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "yarn pre-commit"
}
}
}
React 特有のコーディング課題
JSX と JavaScript の混在による複雑性
React では、JSX 記法により JavaScript と HTML が密接に組み合わされます。この特性により、従来の JavaScript 開発では発生しない独特の課題が生まれます。
JSX における一般的な問題
jsx// 問題1: JSX 要素のキー属性の不適切な使用
const TodoList = ({ todos }) => {
return (
<ul>
{todos.map((todo, index) => (
// インデックスをキーに使用(推奨されない)
<li key={index}>
<TodoItem todo={todo} />
</li>
))}
</ul>
);
};
// 問題2: 不要な Fragment の使用
const UserCard = ({ user }) => {
return (
<React.Fragment>
<div className='user-card'>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
</React.Fragment>
);
};
// 問題3: 危険な HTML の埋め込み
const ArticleContent = ({ htmlContent }) => {
return (
<div
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
);
};
コンポーネント設計における課題
React コンポーネントの設計では、再利用性と保守性のバランスが重要です。
設計上の課題と問題パターン
tsx// 課題1: 過度に複雑なコンポーネント
const ComplexUserDashboard = ({
user,
notifications,
settings,
analytics,
preferences,
}) => {
const [activeTab, setActiveTab] = useState('profile');
const [isEditing, setIsEditing] = useState(false);
const [showNotifications, setShowNotifications] =
useState(false);
// 100行以上のロジックが続く...
const handleProfileUpdate = async (data) => {
// 複雑な更新処理
};
const handleNotificationToggle = () => {
// 通知処理
};
const handleSettingsChange = (key, value) => {
// 設定変更処理
};
// 大量の JSX が続く...
return (
<div className='dashboard'>{/* 複雑な JSX 構造 */}</div>
);
};
// 課題2: Props の型安全性の欠如
const ProductCard = ({ product }) => {
// product の型が不明確
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
{/* product.description が存在するかわからない */}
<p>{product.description}</p>
</div>
);
};
React Hooks の適切な使用に関する課題
React Hooks は強力な機能ですが、不適切な使用により予期しない動作やパフォーマンス問題を引き起こす可能性があります。
Hooks 使用時の典型的な問題
tsx// 問題1: useEffect の依存関係配列の不備
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// userId の変更を検知できない
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.finally(() => setLoading(false));
}, []); // 依存関係が不完全
return loading ? (
<div>Loading...</div>
) : (
<UserDisplay user={user} />
);
};
// 問題2: 不要な再レンダリングを引き起こすuseCallback
const TodoApp = () => {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// filter が変更されるたびに新しい関数が作成される
const addTodo = useCallback(
(text) => {
setTodos((prev) => [
...prev,
{ id: Date.now(), text, completed: false },
]);
},
[filter]
); // filter は不要な依存関係
return (
<div>
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} filter={filter} />
</div>
);
};
ESLint で解決できる React 開発の問題
コード品質の自動的な改善
ESLint は、React 開発における多くの問題を自動的に検出し、修正候補を提示できます。
自動修正可能な問題の例
tsx// 修正前: ESLint が検出する問題
import React, { useState, useEffect } from 'react';
const NewsletterForm = (props) => {
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(false);
// 1. useEffect の依存関係不備
useEffect(() => {
setIsValid(validateEmail(email));
}, []); // email が依存関係に含まれていない
// 2. 不要な Fragment
return (
<React.Fragment>
<form onSubmit={handleSubmit}>
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button disabled={!isValid}>Subscribe</button>
</form>
</React.Fragment>
);
};
// 修正後: ESLint の自動修正適用
import React, { useState, useEffect } from 'react';
const NewsletterForm = (props) => {
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(false);
// 1. 依存関係を正しく設定
useEffect(() => {
setIsValid(validateEmail(email));
}, [email]); // email を依存関係に追加
// 2. 不要な Fragment を削除
return (
<form onSubmit={handleSubmit}>
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button disabled={!isValid}>Subscribe</button>
</form>
);
};
パフォーマンス問題の早期発見
ESLint は、パフォーマンスに影響を与える可能性のあるコードパターンを検出できます。
パフォーマンス関連の問題検出例
tsx// ESLint が検出するパフォーマンス問題
const ProductCatalog = ({ products, categories }) => {
const [selectedCategory, setSelectedCategory] =
useState('all');
// 問題1: 毎回新しいオブジェクトを作成
const filterConfig = {
category: selectedCategory,
sortBy: 'name',
}; // 毎レンダリング時に新しいオブジェクト
// 問題2: インライン関数の使用
return (
<div>
{categories.map((category) => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)} // 毎回新しい関数
className={
selectedCategory === category.id ? 'active' : ''
}
>
{category.name}
</button>
))}
<ProductList
products={products}
filter={filterConfig} // props が毎回変更される
/>
</div>
);
};
// ESLint 推奨の改善版
const ProductCatalog = ({ products, categories }) => {
const [selectedCategory, setSelectedCategory] =
useState('all');
// useMemo でオブジェクトの再作成を防ぐ
const filterConfig = useMemo(
() => ({
category: selectedCategory,
sortBy: 'name',
}),
[selectedCategory]
);
// useCallback で関数の再作成を防ぐ
const handleCategoryChange = useCallback((categoryId) => {
setSelectedCategory(categoryId);
}, []);
return (
<div>
{categories.map((category) => (
<CategoryButton
key={category.id}
category={category}
isActive={selectedCategory === category.id}
onClick={handleCategoryChange}
/>
))}
<ProductList
products={products}
filter={filterConfig}
/>
</div>
);
};
型安全性とバグ予防
TypeScript と連携した ESLint 設定により、型関連のエラーやバグを事前に防げます。
tsx// ESLint + TypeScript が検出する型安全性の問題
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
// 問題: 型定義と実装の不整合
const UserAvatar: React.FC<{ user: User }> = ({ user }) => {
// ESLint が検出: avatar が undefined の可能性
return (
<img
src={user.avatar} // 潜在的な undefined エラー
alt={user.name}
className='user-avatar'
/>
);
};
// 修正版: 型安全な実装
const UserAvatar: React.FC<{ user: User }> = ({ user }) => {
return (
<img
src={user.avatar || '/default-avatar.png'} // デフォルト値で安全性確保
alt={user.name}
className='user-avatar'
/>
);
};
必須の React ESLint ルール設定
基本的なプロジェクト設定
React プロジェクトでの ESLint 設定は、適切な依存関係の管理から始まります。
package.json での依存関係設定
json{
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.50.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-jsx-a11y": "^6.7.0",
"eslint-plugin-import": "^2.28.0",
"eslint-config-prettier": "^9.0.0"
},
"scripts": {
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix"
}
}
基本的な .eslintrc.js 設定
javascriptmodule.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jsx-a11y/recommended',
'prettier', // Prettier との競合を避ける
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint',
'jsx-a11y',
],
settings: {
react: {
version: 'detect', // React バージョンを自動検出
},
},
rules: {
// カスタムルール設定
},
};
重要度別ルール設定
React 開発において特に重要なルールを優先度別に整理します。
高優先度ルール(エラーレベル)
javascriptmodule.exports = {
// ... 基本設定
rules: {
// React Hooks の正しい使用を強制
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// JSX における必須属性
'react/jsx-key': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
// 危険な使用法を防止
'react/no-danger-with-children': 'error',
'react/no-direct-mutation-state': 'error',
'react/no-unescaped-entities': 'error',
// TypeScript との連携
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'error',
},
};
中優先度ルール(警告レベル)
javascriptmodule.exports = {
// ... 基本設定
rules: {
// コンポーネント設計の改善
'react/jsx-pascal-case': 'warn',
'react/no-multi-comp': 'warn',
'react/prefer-stateless-function': 'warn',
// パフォーマンス関連
'react/jsx-no-bind': [
'warn',
{
ignoreDOMComponents: false,
ignoreRefs: true,
allowArrowFunctions: true,
allowFunctions: false,
allowBind: false,
},
],
// コードスタイルの統一
'react/jsx-curly-brace-presence': [
'warn',
{
props: 'never',
children: 'never',
},
],
},
};
プロジェクト固有のカスタムルール
特定のプロジェクト要件に応じたカスタムルールの設定例をご紹介します。
javascriptmodule.exports = {
// ... 基本設定
rules: {
// ファイル命名規則の強制
'import/no-default-export': 'error', // 名前付きエクスポートを推奨
// コンポーネントの最大行数制限
'max-lines': [
'warn',
{
max: 200,
skipBlankLines: true,
skipComments: true,
},
],
// Props の型定義を強制
'react/prop-types': 'off', // TypeScript 使用時は無効化
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
},
],
// カスタムフックの命名規則
'react-hooks/rules-of-hooks': 'error',
'no-restricted-syntax': [
'error',
{
selector:
'FunctionDeclaration[id.name=/^use[A-Z]/]:not([id.name=/^use[A-Z][a-z]/])',
message:
'カスタムフックは use から始まり、キャメルケースで命名してください',
},
],
},
};
コンポーネント設計を改善するルール
再利用性を高めるルール設定
コンポーネントの再利用性を向上させるためのルール設定について詳しく見ていきましょう。
Props インターフェースの明確化
tsx// ESLint ルール: explicit-interface-props
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: (
event: React.MouseEvent<HTMLButtonElement>
) => void;
}
// 良い例: 明確な型定義
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
children,
onClick,
}) => {
const baseClasses = 'btn';
const variantClasses = `btn--${variant}`;
const sizeClasses = `btn--${size}`;
const stateClasses = disabled ? 'btn--disabled' : '';
return (
<button
className={`${baseClasses} ${variantClasses} ${sizeClasses} ${stateClasses}`}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <Spinner size='small' /> : children}
</button>
);
};
// 悪い例: 型定義が曖昧
const BadButton = ({ variant, size, ...props }) => {
// variant や size の型が不明確
return <button {...props} />;
};
コンポーネントの複雑度制限
javascript// .eslintrc.js での設定
module.exports = {
rules: {
// コンポーネントの複雑度を制限
complexity: ['warn', { max: 10 }],
// JSX ネストの深さを制限
'react/jsx-max-depth': ['warn', { max: 4 }],
// コンポーネント内の関数数を制限
'max-lines-per-function': [
'warn',
{
max: 50,
skipBlankLines: true,
skipComments: true,
},
],
// Props の数を制限
'react/jsx-max-props-per-line': [
'warn',
{
maximum: 3,
when: 'multiline',
},
],
},
};
実践的なコンポーネント分割例
tsx// 複雑すぎるコンポーネント(ESLint が警告)
const ComplexProductCard = ({
product,
onAddToCart,
onWishlist,
}) => {
const [quantity, setQuantity] = useState(1);
const [selectedVariant, setSelectedVariant] =
useState(null);
const [showDetails, setShowDetails] = useState(false);
const [reviews, setReviews] = useState([]);
const [rating, setRating] = useState(0);
// 多数の useEffect と複雑なロジック...
return (
<div className='product-card'>
{/* 複雑な JSX 構造 */}
</div>
);
};
// 改善版: 適切に分割されたコンポーネント
const ProductCard: React.FC<ProductCardProps> = ({
product,
}) => {
return (
<div className='product-card'>
<ProductImage
src={product.image}
alt={product.name}
/>
<ProductInfo product={product} />
<ProductActions product={product} />
</div>
);
};
const ProductInfo: React.FC<{ product: Product }> = ({
product,
}) => {
return (
<div className='product-info'>
<h3>{product.name}</h3>
<ProductPrice
price={product.price}
discount={product.discount}
/>
<ProductRating
rating={product.rating}
reviewCount={product.reviewCount}
/>
</div>
);
};
const ProductActions: React.FC<{ product: Product }> = ({
product,
}) => {
const [quantity, setQuantity] = useState(1);
return (
<div className='product-actions'>
<QuantitySelector
value={quantity}
onChange={setQuantity}
/>
<AddToCartButton
product={product}
quantity={quantity}
/>
<WishlistButton productId={product.id} />
</div>
);
};
命名規則とファイル構造
一貫した命名規則により、コードの可読性と保守性が大幅に向上します。
コンポーネント命名規則の強制
javascript// .eslintrc.js カスタムルール
module.exports = {
rules: {
// コンポーネントファイル名の規則
'filename-rules/match': [
2,
{
'.tsx': /^[A-Z][A-Za-z]*\.tsx$/,
},
],
// コンポーネント名とファイル名の一致
'react/jsx-pascal-case': [
'error',
{
allowAllCaps: false,
ignore: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
},
],
// Props 型の命名規則
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '.*Props$',
match: true,
},
},
],
},
};
フォルダ構造の最適化
typescript// 推奨されるコンポーネント構造
src/
├── components/
│ ├── ui/ // 再利用可能な UI コンポーネント
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ └── index.ts
│ │ └── Input/
│ └── feature/ // 機能固有のコンポーネント
│ ├── ProductCard/
│ └── UserProfile/
├── hooks/ // カスタムフック
├── types/ // 型定義
└── utils/ // ユーティリティ関数
// index.ts での適切なエクスポート
export { Button } from './Button';
export type { ButtonProps } from './Button';
// ESLint での import 順序の強制
module.exports = {
rules: {
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
},
};
パフォーマンス最適化に役立つルール
不要な再レンダリングを防止するルール
React アプリケーションのパフォーマンス向上において、不要な再レンダリングの防止は極めて重要です。ESLint を活用して、これらの問題を早期に検出できます。
useCallback と useMemo の適切な使用
tsx// ESLint カスタムルール: パフォーマンス最適化の強制
module.exports = {
rules: {
// インライン関数の使用を警告
'react/jsx-no-bind': [
'warn',
{
ignoreDOMComponents: false,
ignoreRefs: true,
allowArrowFunctions: false,
allowFunctions: false,
allowBind: false,
},
],
// useMemo の不適切な使用を検出
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks:
'(useRecoilCallback|useRecoilTransaction_UNSTABLE)',
},
],
},
};
// 問題のあるコード例
const ProductList = ({ products, onProductClick }) => {
const [sortOrder, setSortOrder] = useState('name');
return (
<div>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
// 毎回新しい関数が作成される(ESLint が警告)
onClick={() => onProductClick(product.id)}
// 毎回新しいオブジェクトが作成される
style={{ margin: '10px' }}
/>
))}
</div>
);
};
// 最適化されたコード例
const ProductList = ({ products, onProductClick }) => {
const [sortOrder, setSortOrder] = useState('name');
// useCallback で関数をメモ化
const handleProductClick = useCallback(
(productId: string) => {
onProductClick(productId);
},
[onProductClick]
);
// useMemo でスタイルオブジェクトをメモ化
const cardStyle = useMemo(() => ({ margin: '10px' }), []);
// useMemo でソートされた商品リストをメモ化
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => {
if (sortOrder === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
}, [products, sortOrder]);
return (
<div>
{sortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onClick={handleProductClick}
style={cardStyle}
/>
))}
</div>
);
};
メモ化の適切な活用
React.memo、useMemo、useCallback の使い分けを ESLint で管理できます。
メモ化のベストプラクティス
tsx// ESLint 設定: メモ化の適切な使用を促進
module.exports = {
rules: {
// React.memo の使用を推奨
'react/display-name': 'warn',
// カスタムルール: 重いコンポーネントにはメモ化を強制
'custom-rules/require-memo-for-heavy-components':
'warn',
},
};
// 重いコンポーネントの例
interface DataVisualizationProps {
data: number[];
width: number;
height: number;
options: ChartOptions;
}
// React.memo を使用した最適化
const DataVisualization =
React.memo<DataVisualizationProps>(
({ data, width, height, options }) => {
// 重い計算処理をメモ化
const processedData = useMemo(() => {
return data.map((value, index) => ({
x: index,
y: value,
processed: complexCalculation(value),
}));
}, [data]);
// SVG パスの生成をメモ化
const svgPath = useMemo(() => {
return generateSVGPath(
processedData,
width,
height
);
}, [processedData, width, height]);
return (
<svg width={width} height={height}>
<path d={svgPath} stroke='blue' fill='none' />
</svg>
);
},
(prevProps, nextProps) => {
// カスタム比較関数
return (
prevProps.width === nextProps.width &&
prevProps.height === nextProps.height &&
arraysEqual(prevProps.data, nextProps.data) &&
objectsEqual(prevProps.options, nextProps.options)
);
}
);
DataVisualization.displayName = 'DataVisualization';
バンドルサイズ最適化のルール
不要なインポートやコードの削除により、バンドルサイズを最適化できます。
javascript// バンドルサイズ最適化の ESLint 設定
module.exports = {
rules: {
// 未使用のインポートを検出
'unused-imports/no-unused-imports': 'error',
// tree-shaking に最適なインポート方法を強制
'import/no-commonjs': 'error',
// 大きなライブラリの全体インポートを防止
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['lodash'],
message:
'lodash は個別インポートを使用してください。例: import debounce from "lodash/debounce"',
},
{
group: ['@mui/material'],
message:
'MUI は個別インポートを使用してください。例: import Button from "@mui/material/Button"',
},
],
},
],
// 動的インポートの推奨
'import/dynamic-import-chunkname': 'warn',
},
};
// 良い例: 最適化されたインポート
import { debounce } from 'lodash-es'; // tree-shaking 対応版
import Button from '@mui/material/Button'; // 個別インポート
// React.lazy での動的インポート
const HeavyComponent = React.lazy(() =>
import(
/* webpackChunkName: "heavy-component" */ './HeavyComponent'
)
);
// 悪い例: 最適化されていないインポート
import _ from 'lodash'; // 全体をインポート
import * as MUI from '@mui/material'; // 全体をインポート
アクセシビリティ向上のためのルール
jsx-a11y プラグインの活用
アクセシビリティの向上は、すべてのユーザーにとって使いやすいアプリケーションを作る上で欠かせません。
基本的なアクセシビリティルール
javascript// jsx-a11y の包括的な設定
module.exports = {
extends: ['plugin:jsx-a11y/recommended'],
rules: {
// より厳格なアクセシビリティチェック
'jsx-a11y/alt-text': [
'error',
{
elements: [
'img',
'object',
'area',
'input[type="image"]',
],
img: ['Image'],
object: ['Object'],
area: ['Area'],
'input[type="image"]': ['InputImage'],
},
],
// ボタンとリンクの適切な使い分け
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['to'],
aspects: ['noHref', 'invalidHref', 'preferButton'],
},
],
// フォーカス管理
'jsx-a11y/no-autofocus': [
'error',
{ ignoreNonDOM: true },
],
// ARIA ルール
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-proptypes': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/role-has-required-aria-props': 'error',
'jsx-a11y/role-supports-aria-props': 'error',
},
};
実践的なアクセシビリティ実装例
tsx// 問題のあるアクセシビリティ実装
const BadModal = ({ isOpen, onClose, children }) => {
return isOpen ? (
<div className='modal-overlay' onClick={onClose}>
<div className='modal-content'>
<button onClick={onClose}>×</button>
{children}
</div>
</div>
) : null;
};
// アクセシビリティを考慮した実装
const AccessibleModal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
}) => {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// モーダル表示時、前のフォーカス要素を記録
previousFocusRef.current =
document.activeElement as HTMLElement;
// モーダル内の最初のフォーカス可能要素にフォーカス
const focusableElements =
modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (
focusableElements &&
focusableElements.length > 0
) {
(focusableElements[0] as HTMLElement).focus();
}
} else {
// モーダル閉じる時、前のフォーカス要素に戻す
previousFocusRef.current?.focus();
}
}, [isOpen]);
// ESC キーでモーダルを閉じる
useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
onClose();
}
};
document.addEventListener('keydown', handleEscapeKey);
return () =>
document.removeEventListener(
'keydown',
handleEscapeKey
);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
className='modal-overlay'
onClick={onClose}
role='dialog'
aria-modal='true'
aria-labelledby='modal-title'
>
<div
ref={modalRef}
className='modal-content'
onClick={(e) => e.stopPropagation()}
>
<header className='modal-header'>
<h2 id='modal-title'>{title}</h2>
<button
onClick={onClose}
aria-label='モーダルを閉じる'
className='modal-close-button'
>
<span aria-hidden='true'>×</span>
</button>
</header>
<div className='modal-body'>{children}</div>
</div>
</div>
);
};
セマンティック HTML とスクリーンリーダー対応
適切な HTML 要素の使用により、スクリーンリーダーでの操作性が大幅に向上します。
tsx// セマンティック HTML の活用例
const ProductSearchForm: React.FC = () => {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [priceRange, setPriceRange] = useState([0, 1000]);
return (
<form role='search' aria-label='商品検索'>
<fieldset>
<legend>検索条件</legend>
{/* 適切なラベル付け */}
<div className='form-group'>
<label htmlFor='search-query'>商品名で検索</label>
<input
id='search-query'
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-describedby='search-hint'
placeholder='商品名を入力'
/>
<div id='search-hint' className='form-hint'>
商品名の一部でも検索可能です
</div>
</div>
{/* セレクトボックスの適切な実装 */}
<div className='form-group'>
<label htmlFor='category-select'>カテゴリ</label>
<select
id='category-select'
value={category}
onChange={(e) => setCategory(e.target.value)}
aria-describedby='category-description'
>
<option value='all'>すべてのカテゴリ</option>
<option value='electronics'>電子機器</option>
<option value='clothing'>衣料品</option>
<option value='books'>書籍</option>
</select>
<div
id='category-description'
className='sr-only'
>
検索対象のカテゴリを選択してください
</div>
</div>
{/* 価格範囲スライダー */}
<div className='form-group'>
<fieldset>
<legend>価格範囲</legend>
<div className='price-range'>
<label htmlFor='price-min'>
最低価格: ¥{priceRange[0]}
</label>
<input
id='price-min'
type='range'
min='0'
max='1000'
value={priceRange[0]}
onChange={(e) =>
setPriceRange([
parseInt(e.target.value),
priceRange[1],
])
}
aria-describedby='price-range-description'
/>
<label htmlFor='price-max'>
最高価格: ¥{priceRange[1]}
</label>
<input
id='price-max'
type='range'
min='0'
max='1000'
value={priceRange[1]}
onChange={(e) =>
setPriceRange([
priceRange[0],
parseInt(e.target.value),
])
}
aria-describedby='price-range-description'
/>
<div
id='price-range-description'
className='sr-only'
>
商品の価格範囲を設定してください
</div>
</div>
</fieldset>
</div>
</fieldset>
<button type='submit' className='search-button'>
<span className='button-icon' aria-hidden='true'>
🔍
</span>
検索実行
</button>
</form>
);
};
保守性を高める高度なルール設定
コード複雑度の管理
長期的な保守性を確保するため、コード複雑度を適切に管理することが重要です。
複雑度制限の詳細設定
javascript// 高度な複雑度管理設定
module.exports = {
rules: {
// 循環的複雑度の制限
complexity: ['warn', { max: 8 }],
// 認知的複雑度の制限(より直感的な複雑度指標)
'sonarjs/cognitive-complexity': ['warn', 15],
// 関数の最大行数制限
'max-lines-per-function': [
'warn',
{
max: 50,
skipBlankLines: true,
skipComments: true,
IIFEs: true,
},
],
// ネストの深さ制限
'max-depth': ['warn', 4],
// 1つのファイル内のクラス/関数数制限
'max-classes-per-file': ['warn', 3],
// パラメータ数の制限
'max-params': ['warn', 4],
},
};
// 複雑すぎる関数の例(ESLint が警告)
const ComplexOrderProcessor = (
order,
user,
inventory,
promotions,
shippingOptions
) => {
if (order.items && order.items.length > 0) {
for (let i = 0; i < order.items.length; i++) {
const item = order.items[i];
if (inventory[item.id]) {
if (inventory[item.id].quantity >= item.quantity) {
if (user.membershipLevel === 'premium') {
if (
promotions.some((p) =>
p.applicableItems.includes(item.id)
)
) {
if (item.category === 'electronics') {
// さらに深いネスト...
}
}
}
}
}
}
}
// 複雑な処理が続く...
};
// 改善版: 単一責任原則に基づく分割
const OrderProcessor = {
validateOrder: (order: Order): ValidationResult => {
if (!order.items || order.items.length === 0) {
return {
isValid: false,
error: '商品が選択されていません',
};
}
return { isValid: true };
},
checkInventory: (
items: OrderItem[],
inventory: Inventory
): InventoryCheckResult => {
const unavailableItems = items.filter(
(item) =>
!inventory[item.id] ||
inventory[item.id].quantity < item.quantity
);
return {
isAvailable: unavailableItems.length === 0,
unavailableItems,
};
},
applyPromotions: (
order: Order,
user: User,
promotions: Promotion[]
): Order => {
return promotions.reduce(
(processedOrder, promotion) =>
PromotionService.apply(
processedOrder,
user,
promotion
),
order
);
},
calculateShipping: (
order: Order,
options: ShippingOptions
): ShippingCost => {
return ShippingCalculator.calculate(order, options);
},
};
ドキュメンテーションルール
コードの自己文書化を促進するルール設定です。
typescript// JSDoc コメントを強制するルール
module.exports = {
plugins: ['jsdoc'],
rules: {
// 関数にJSDocコメントを要求
'jsdoc/require-jsdoc': [
'warn',
{
require: {
FunctionDeclaration: true,
MethodDefinition: true,
ClassDeclaration: true,
ArrowFunctionExpression: true,
},
},
],
// JSDocの形式チェック
'jsdoc/check-param-names': 'warn',
'jsdoc/check-return-type': 'warn',
'jsdoc/require-param-description': 'warn',
'jsdoc/require-returns-description': 'warn',
// TypeScript との整合性チェック
'jsdoc/match-description': [
'warn',
{ matchDescription: '^[A-Z].*\\.$' },
],
},
};
/**
* ユーザーの注文を処理し、在庫確認から決済まで一貫して実行します。
*
* @param order - 処理する注文情報
* @param user - 注文者のユーザー情報
* @param options - 処理オプション(決済方法、配送オプションなど)
* @returns 処理結果を含むPromise
*
* @example
* ```typescript
* const result = await processOrder(
* { items: [{ id: '1', quantity: 2 }] },
* { id: 'user123', membershipLevel: 'premium' },
* { paymentMethod: 'creditCard' }
* );
* ```
*
* @throws {InsufficientInventoryError} 在庫不足の場合
* @throws {PaymentFailedError} 決済処理に失敗した場合
*/
async function processOrder(
order: Order,
user: User,
options: ProcessingOptions
): Promise<OrderResult> {
// 実装...
}
テスト可能性を向上させるルール
テストしやすいコードの作成を促進するルール設定です。
javascript// テスタビリティ向上のためのルール
module.exports = {
rules: {
// 依存性注入の推奨
'prefer-dependency-injection': 'warn',
// 副作用のある関数の分離
'no-side-effects-in-computed': 'error',
// テスト用の data-testid 属性の使用推奨
'testing-library/prefer-screen-queries': 'warn',
'testing-library/no-node-access': 'warn',
'testing-library/no-container': 'warn',
},
};
// テストしにくいコードの例
const UserDashboard = () => {
const [user, setUser] = useState(null);
useEffect(() => {
// 直接APIを呼び出し(テストが困難)
fetch('/api/user')
.then((res) => res.json())
.then(setUser);
}, []);
const handleLogout = () => {
// 直接localStorageを操作(テストが困難)
localStorage.removeItem('token');
window.location.href = '/login';
};
return (
<div>
{user && <h1>Welcome, {user.name}</h1>}
<button onClick={handleLogout}>Logout</button>
</div>
);
};
// テストしやすいコードの例
interface UserDashboardProps {
userService: UserService;
authService: AuthService;
router: Router;
}
const UserDashboard: React.FC<UserDashboardProps> = ({
userService,
authService,
router,
}) => {
const [user, setUser] = (useState < User) | (null > null);
useEffect(() => {
// 依存性注入されたサービスを使用
userService
.getCurrentUser()
.then(setUser)
.catch(console.error);
}, [userService]);
const handleLogout = useCallback(async () => {
try {
await authService.logout();
router.navigate('/login');
} catch (error) {
console.error('Logout failed:', error);
}
}, [authService, router]);
return (
<div data-testid='user-dashboard'>
{user && (
<h1 data-testid='welcome-message'>
Welcome, {user.name}
</h1>
)}
<button
data-testid='logout-button'
onClick={handleLogout}
>
Logout
</button>
</div>
);
};
まとめ
React 開発における ESLint の活用は、単なるコードフォーマッターの域を超えて、開発チーム全体の生産性と品質を大幅に向上させる重要な投資です。この記事でご紹介したルール設定により、バグの早期発見からパフォーマンス最適化まで、包括的なコード品質管理が実現できます。
効果的な ESLint 活用のポイント
適切なルール設定により、React 特有の課題を自動的に検出し、修正できるようになります。特に、React Hooks の依存関係配列やコンポーネントの複雑度管理は、アプリケーションの安定性に直結する重要な要素です。
継続的な改善とチーム開発
ESLint の真価は、個人の開発だけでなく、チーム開発において発揮されます。一貫したコーディング規約と自動化されたチェック機能により、コードレビューの効率化と品質の標準化が実現できるでしょう。
長期的な保守性の確保
適切なルール設定は、プロジェクトの長期的な保守性を大幅に向上させます。コード複雑度の管理やアクセシビリティの確保など、将来の課題を事前に防ぐことができます。
この記事で学んだ設定を基に、プロジェクトの特性に応じてカスタマイズし、より良い React 開発環境を構築していってください。ESLint を効果的に活用することで、開発の効率性と品質の両方を同時に向上させることができるはずです。
関連リンク
- review
チーム開発が劇的に変わった!『リーダブルコード』Dustin Boswell & Trevor Foucher
- review
アジャイル初心者でも大丈夫!『アジャイルサムライ − 達人開発者への道』Jonathan Rasmusson
- review
人生が作品になる!『自分の中に毒を持て』岡本太郎
- review
体調不良の 99%が解決!『眠れなくなるほど面白い 図解 自律神経の話』小林弘幸著で学ぶ、現代人必須の自律神経コントロール術と人生を変える健康革命
- review
衝撃の事実!『睡眠こそ最強の解決策である』マシュー・ウォーカー著が明かす、99%の人が知らない睡眠の驚くべき真実と人生を変える科学的メカニズム
- review
人生が激変!『嫌われる勇気』岸見一郎・古賀史健著から学ぶ、アドラー心理学で手に入れる真の幸福と自己実現