Reactディレクトリ構成のベストプラクティス|初心者向け設計指針
React アプリケーションのディレクトリ構成で迷っている初心者向けに、保守性と拡張性を考慮したベストプラクティスを解説。プロジェクト規模別の構成パターン、実際の企業でよく使われる設計指針、リファクタリング方法を詳しく紹介します。
みなさん、Reactアプリを作る時に「ファイルをどこに置けばいいの?」と迷ったことはありませんか?
「コンポーネントが増えてきて、どう整理すればいいかわからない」 「チームで開発する時、みんなが分かりやすい構成にしたい」 「プロジェクトが大きくなった時のことを考えて設計したい」
こんな悩みを抱えている方も多いですよね。
この記事では、Reactアプリケーションのディレクトリ構成について、初心者でも理解しやすいベストプラクティスを詳しく解説します。 小さなプロジェクトから大規模なものまで、段階的に成長させられる構成パターンをご紹介しますよ。
ぜひ最後まで読んで、保守性の高いReactアプリを作ってみてください!
なぜディレクトリ構成が重要なの?
まず、なぜディレクトリ構成について考える必要があるのでしょうか?
良い構成がもたらすメリット
適切なディレクトリ構成は、開発がとても楽になります。
ファイルを見つけやすくなる
良い構成だと、必要なファイルをすぐに見つけられます。 例えば、ログイン関連のコンポーネントを探す時、どこにあるかすぐに分かります。
src/
├── components/
│ ├── common/ # 共通で使うコンポーネント
│ ├── forms/ # フォーム関連
│ └── layout/ # レイアウト関連
├── pages/ # ページコンポーネント
├── hooks/ # カスタムフック
└── utils/ # ユーティリティ関数
この構成を見ただけで、ログインフォームはforms/
にありそうだな、とすぐに分かりますよね。
コードを理解しやすくなる
ファイルの役割が明確だと、コード全体の流れが理解しやすくなります。 新しくチームに入った人も、迷わずに開発に参加できます。
変更の影響範囲が分かりやすくなる
機能ごとにファイルが整理されていると、修正した時の影響範囲を予測しやすくなります。 バグ修正や機能追加が、とても安全に行えるようになりますよ。
悪い構成の例と問題点
逆に、適切でない構成は開発を困難にします。
src/
├── App.js
├── component1.js # 何のコンポーネント?
├── component2.js # 役割が不明
├── component3.js
├── helper.js # 何のヘルパー?
├── style.css
├── util.js
└── data.js
この構成だと、以下のような問題が起こります:
ファイルの役割が分からない
component1.js
って何のコンポーネントでしょうか?
ファイル名からは全く想像できませんね。
関連するファイルを見つけにくい ログイン機能を修正したい時、どのファイルを見ればいいか分からなくなります。
プロジェクトが大きくなると管理できない ファイル数が増えてくると、もう手に負えなくなってしまいます。
チーム開発での重要性
一人で開発する時よりも、チーム開発では統一された構成が特に重要です。
新しいメンバーが理解しやすい 明確な構成ルールがあると、新しい人もすぐに開発に参加できます。
コードレビューが効率的 どこに何があるか分かっていると、レビューもスムーズに進みます。
知識の共有ができる 「なんとなく」で決めていたことを、きちんとしたルールにできます。
// 良い例: 役割が明確なコンポーネント
// src/components/forms/UserRegistrationForm.jsx
import React, { useState } from 'react';
import { validateEmail, validatePassword } from '../../utils/validation';
import { Button } from '../common/Button';
import { InputField } from '../common/InputField';
const UserRegistrationForm = ({ onSubmit }) => {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
// バリデーション
const newErrors = {};
if (!validateEmail(formData.email)) {
newErrors.email = 'メールアドレスの形式が正しくありません';
}
if (!validatePassword(formData.password)) {
newErrors.password = 'パスワードは8文字以上である必要があります';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'パスワードが一致しません';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<InputField
label="メールアドレス"
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
error={errors.email}
/>
<InputField
label="パスワード"
type="password"
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
error={errors.password}
/>
<InputField
label="パスワード確認"
type="password"
value={formData.confirmPassword}
onChange={(e) => setFormData({...formData, confirmPassword: e.target.value})}
error={errors.confirmPassword}
/>
<Button type="submit">登録</Button>
</form>
);
};
export default UserRegistrationForm;
このコードを見ると、どんな処理をしているかが一目で分かりますね。
ファイルの場所もforms/
ディレクトリにあることで、フォーム関連のコンポーネントだと分かります。
適切な構成は、コードの品質を保つためにとても重要なんです。
基本的な構成パターン
Reactアプリケーションの基本的な構成パターンを見ていきましょう。
Create React Appのデフォルト構成
まずは、Create React Appで作られるデフォルトの構成から。
my-app/
├── public/
│ ├── index.html
│ ├── favicon.ico
│ └── manifest.json
├── src/
│ ├── App.js
│ ├── App.css
│ ├── App.test.js
│ ├── index.js
│ ├── index.css
│ └── logo.svg
├── package.json
└── README.md
各ディレクトリの役割を簡単に説明しますね:
- public/:HTMLファイルや画像など、そのまま使うファイル
- src/:Reactのコンポーネントやロジック
- package.json:プロジェクトの設定と使用するライブラリの情報
この構成は、小さなプロジェクトには十分です。 でも、ファイルが増えてくると整理が必要になってきます。
小規模プロジェクト向け構成
10-20個程度のコンポーネントがある小規模プロジェクトに適した構成です。
src/
├── components/
│ ├── Header.jsx
│ ├── Footer.jsx
│ ├── Navigation.jsx
│ └── Button.jsx
├── pages/
│ ├── Home.jsx
│ ├── About.jsx
│ └── Contact.jsx
├── hooks/
│ ├── useApi.js
│ └── useLocalStorage.js
├── utils/
│ ├── api.js
│ └── helpers.js
├── styles/
│ ├── global.css
│ └── components.css
├── App.jsx
└── index.js
この構成のポイント:
- components/:再利用可能なコンポーネント
- pages/:各ページのメインコンポーネント
- hooks/:カスタムフック
- utils/:便利な関数たち
- styles/:スタイルファイル
// src/components/Header.jsx
import React from 'react';
import Navigation from './Navigation';
const Header = () => {
return (
<header className="header">
<div className="container">
<h1 className="logo">My App</h1>
<Navigation />
</div>
</header>
);
};
export default Header;
このHeaderコンポーネントは、シンプルで分かりやすいですね。
Navigation
コンポーネントを読み込んで使っています。
// src/pages/Home.jsx
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const Home = () => {
return (
<div className="page">
<Header />
<main className="main">
<h2>ホームページ</h2>
<p>ようこそ、私たちのサイトへ!</p>
</main>
<Footer />
</div>
);
};
export default Home;
ページコンポーネントでは、共通のコンポーネントを組み合わせて画面を作っています。
// src/hooks/useApi.js
import { useState, useEffect } from 'react';
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useApi;
このカスタムフックは、API呼び出しを簡単にしてくれます。 どのコンポーネントからでも使えるので、とても便利ですよ。
中規模プロジェクト向け構成
20-50個程度のコンポーネントがある中規模プロジェクトでは、もう少し細かく分類します。
src/
├── components/
│ ├── common/
│ │ ├── Button/
│ │ │ ├── Button.jsx
│ │ │ ├── Button.module.css
│ │ │ └── index.js
│ │ ├── Modal/
│ │ └── Loading/
│ ├── forms/
│ │ ├── LoginForm/
│ │ ├── SignupForm/
│ │ └── ContactForm/
│ └── layout/
│ ├── Header/
│ ├── Footer/
│ └── Sidebar/
├── pages/
│ ├── Home/
│ ├── About/
│ ├── Products/
│ └── Contact/
├── hooks/
│ ├── useAuth.js
│ ├── useApi.js
│ └── useLocalStorage.js
├── context/
│ ├── AuthContext.js
│ └── ThemeContext.js
├── services/
│ ├── api.js
│ ├── auth.js
│ └── storage.js
├── utils/
│ ├── validators.js
│ ├── formatters.js
│ └── constants.js
├── styles/
│ ├── globals.css
│ ├── variables.css
│ └── components.css
├── App.jsx
└── index.js
新しく追加された要素:
- context/:Reactのコンテキスト(グローバルな状態管理)
- services/:API呼び出しなどの外部サービスとの連携
- 各コンポーネントごとのディレクトリ:関連ファイルをまとめて管理
// src/components/common/Button/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Button.module.css';
const Button = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
...props
}) => {
const className = [
styles.button,
styles[variant],
styles[size],
loading && styles.loading
].filter(Boolean).join(' ');
return (
<button
type={type}
className={className}
onClick={onClick}
disabled={disabled || loading}
{...props}
>
{children}
</button>
);
};
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary', 'outline']),
size: PropTypes.oneOf(['small', 'medium', 'large']),
disabled: PropTypes.bool,
loading: PropTypes.bool,
onClick: PropTypes.func,
type: PropTypes.oneOf(['button', 'submit', 'reset'])
};
export default Button;
このButtonコンポーネントは、いろんなバリエーションに対応できます。
PropTypes
を使って、どんなpropsが使えるかも明確にしています。
// src/components/common/Button/index.js
export { default } from './Button';
このindex.jsファイルがあることで、インポートがシンプルになります。
// 使う時はこう書ける
import Button from '../components/common/Button';
// index.jsがなかったら、こう書く必要がある
import Button from '../components/common/Button/Button';
大規模プロジェクト向け構成
50個以上のコンポーネントがある大規模プロジェクトでは、機能別に構成します。
src/
├── components/
│ ├── common/
│ ├── forms/
│ └── layout/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ ├── dashboard/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ └── user-management/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── types/
├── shared/
│ ├── hooks/
│ ├── services/
│ ├── utils/
│ ├── types/
│ └── constants/
├── store/
│ ├── slices/
│ ├── middleware/
│ └── index.js
├── App.jsx
└── index.js
大規模プロジェクトの特徴:
- features/:機能ごとにファイルをまとめる
- shared/:複数の機能で共通で使うもの
- store/:状態管理(ReduxやZustandなど)
// src/features/auth/components/LoginForm.jsx
import React, { useState } from 'react';
import { useAuth } from '../hooks/useAuth';
import { validateLoginData } from '../utils/validation';
import { Button } from '../../../components/common/Button';
import { InputField } from '../../../components/common/InputField';
const LoginForm = () => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const { login, loading } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
const validationErrors = validateLoginData(formData);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
await login(formData);
} catch (error) {
setErrors({ submit: error.message });
}
};
return (
<form onSubmit={handleSubmit}>
<InputField
label="メールアドレス"
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
error={errors.email}
/>
<InputField
label="パスワード"
type="password"
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
error={errors.password}
/>
{errors.submit && <p className="error">{errors.submit}</p>}
<Button type="submit" loading={loading}>
ログイン
</Button>
</form>
);
};
export default LoginForm;
この構成では、認証機能に関するすべてのファイルがauth/
ディレクトリにまとまっています。
機能を修正したり、新しい人が開発に参加する時も、どこを見ればいいかすぐに分かりますね。
プロジェクトの規模に応じて、このように段階的に構成を発展させていくのがおすすめです。
ファイル命名規則
一貫した命名規則は、プロジェクトをとても分かりやすくしてくれます。
コンポーネントファイルの命名
基本的な命名規則
ReactコンポーネントはPascalCase(最初の文字が大文字)で命名します。
// ✅ 良い例: PascalCase を使用
UserProfile.jsx
LoginForm.jsx
ProductCard.jsx
NavigationBar.jsx
// ❌ 悪い例
userprofile.jsx
loginform.js
product-card.jsx
navigation_bar.jsx
ファイル拡張子の統一
プロジェクト内で拡張子を統一することが大切です。
// React コンポーネント
.jsx または .js
// TypeScript を使う場合
.tsx または .ts
// プロジェクト内で統一することが重要
チーム内で「Reactコンポーネントは.jsxにしよう」と決めたら、みんなでそのルールに従いましょう。
ディレクトリ命名規則
ディレクトリ名はkebab-case(単語をハイフンで繋ぐ)を使います。
src/
├── components/
│ ├── user-profile/
│ ├── login-form/
│ ├── product-card/
│ └── navigation-bar/
├── pages/
│ ├── user-dashboard/
│ ├── product-list/
│ └── checkout-process/
└── utils/
├── date-helpers/
├── form-validation/
└── api-client/
なぜkebab-caseがいいの?
- URLに使いやすい:そのままルーティングで使える
- 大文字小文字の問題を避けられる:OSによる違いを気にしなくていい
- 読みやすい:単語の区切りがはっきり分かる
インデックスファイルの活用
index.js
ファイルを使うと、インポートがとてもシンプルになります。
// src/components/common/Button/index.js
export { default } from './Button';
export { ButtonGroup } from './ButtonGroup';
export { IconButton } from './IconButton';
使う時の違い:
// index.js がある場合
import Button, { ButtonGroup, IconButton } from '../components/common/Button';
// index.js がない場合
import Button from '../components/common/Button/Button';
import { ButtonGroup } from '../components/common/Button/ButtonGroup';
import { IconButton } from '../components/common/Button/IconButton';
だいぶスッキリしますよね!
実用的な命名例
機能別の命名パターン
// フォーム関連
LoginForm.jsx
SignupForm.jsx
ContactForm.jsx
UserProfileForm.jsx
// レイアウト関連
Header.jsx
Footer.jsx
Sidebar.jsx
MainLayout.jsx
// 共通コンポーネント
Button.jsx
Modal.jsx
Loading.jsx
ErrorBoundary.jsx
// ページコンポーネント
HomePage.jsx
AboutPage.jsx
ProductListPage.jsx
UserDashboardPage.jsx
// カスタムフック
useAuth.js
useApi.js
useLocalStorage.js
useDebounce.js
// ユーティリティ
dateHelpers.js
formValidation.js
apiClient.js
constants.js
コンポーネントの種類別命名
// 1. 基本コンポーネント
// UserCard.jsx
const UserCard = ({ user }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
// 2. 容器コンポーネント
// UserCardContainer.jsx
const UserCardContainer = ({ userId }) => {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <Loading />;
if (error) return <ErrorMessage error={error} />;
return <UserCard user={user} />;
};
// 3. ページコンポーネント
// UserProfilePage.jsx
const UserProfilePage = () => {
const { userId } = useParams();
return (
<div className="page">
<PageHeader title="ユーザープロフィール" />
<UserCardContainer userId={userId} />
</div>
);
};
// 4. レイアウトコンポーネント
// DashboardLayout.jsx
const DashboardLayout = ({ children }) => {
return (
<div className="dashboard-layout">
<Header />
<div className="dashboard-content">
<Sidebar />
<main className="dashboard-main">
{children}
</main>
</div>
<Footer />
</div>
);
};
それぞれのコンポーネントの役割が、名前から想像できますね。
Containerがついているものはデータを取得する役割、Pageがついているものはページ全体を表示する役割、といった感じです。
適切な命名規則により、コードがとても分かりやすくなります。 プロジェクトを始める時に、チーム全体で命名ルールを決めておくことをおすすめします。
状態管理の構成
Reactアプリケーションの状態管理は、プロジェクトの規模に応じて適切な手法を選ぶことが大切です。
状態管理の選択肢
プロジェクト規模別の推奨手法
規模 | 推奨手法 | 理由 |
---|---|---|
小規模 | useState + useContext | シンプルで覚えやすい |
中規模 | Context API + useReducer | 複雑な状態も管理できる |
大規模 | Redux Toolkit | 予測しやすく、拡張しやすい |
それぞれの特徴を見ていきましょう。
Context APIを使った構成
小〜中規模プロジェクトでは、ReactのContext APIがおすすめです。
// src/context/AppContext.js
import React, { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
const initialState = {
user: null,
theme: 'light',
language: 'ja',
notifications: [],
loading: false
};
const appReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'SET_LANGUAGE':
return { ...state, language: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'REMOVE_NOTIFICATION':
return {
...state,
notifications: state.notifications.filter(
(notification) => notification.id !== action.payload
)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
default:
return state;
}
};
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, initialState);
const actions = {
setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
setLanguage: (language) => dispatch({ type: 'SET_LANGUAGE', payload: language }),
addNotification: (notification) =>
dispatch({ type: 'ADD_NOTIFICATION', payload: notification }),
removeNotification: (id) =>
dispatch({ type: 'REMOVE_NOTIFICATION', payload: id }),
setLoading: (loading) => dispatch({ type: 'SET_LOADING', payload: loading })
};
return (
<AppContext.Provider value={{ state, actions }}>
{children}
</AppContext.Provider>
);
};
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
};
このContext APIの仕組みを詳しく見てみましょう。
初期状態の定義
initialState
でアプリ全体で使う状態を定義しています。
ユーザー情報、テーマ、言語設定などですね。
Reducerで状態の更新
appReducer
関数で、どのような操作で状態を変更するかを定義しています。
SET_USER
でユーザーを設定、SET_THEME
でテーマを変更、といった感じです。
Providerで値を提供
AppProvider
コンポーネントで、アプリ全体に状態と操作用の関数を提供しています。
カスタムフックで使いやすく
useApp
フックを作ることで、どのコンポーネントからでも簡単に状態にアクセスできます。
複数のContextを使う場合
大きなアプリでは、機能ごとにContextを分けることもあります。
// src/context/index.js
import React from 'react';
import { AuthProvider } from './AuthContext';
import { ThemeProvider } from './ThemeContext';
import { NotificationProvider } from './NotificationContext';
export const AppProviders = ({ children }) => {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
};
こうすることで、認証に関する状態、テーマに関する状態、通知に関する状態を別々に管理できます。
// src/context/AuthContext.js
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { authService } from '../services/authService';
const AuthContext = createContext();
const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN_START':
return { ...state, loading: true, error: null };
case 'LOGIN_SUCCESS':
return {
...state,
user: action.payload,
isAuthenticated: true,
loading: false
};
case 'LOGIN_FAILURE':
return {
...state,
error: action.payload,
loading: false
};
case 'LOGOUT':
return {
...state,
user: null,
isAuthenticated: false,
loading: false
};
case 'UPDATE_USER':
return { ...state, user: { ...state.user, ...action.payload } };
default:
return state;
}
};
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isAuthenticated: false,
loading: false,
error: null
});
useEffect(() => {
const initializeAuth = async () => {
try {
const token = localStorage.getItem('token');
if (token) {
dispatch({ type: 'LOGIN_START' });
const user = await authService.getCurrentUser();
dispatch({ type: 'LOGIN_SUCCESS', payload: user });
}
} catch (error) {
dispatch({ type: 'LOGIN_FAILURE', payload: error.message });
}
};
initializeAuth();
}, []);
const login = async (credentials) => {
try {
dispatch({ type: 'LOGIN_START' });
const response = await authService.login(credentials);
localStorage.setItem('token', response.token);
dispatch({ type: 'LOGIN_SUCCESS', payload: response.user });
} catch (error) {
dispatch({ type: 'LOGIN_FAILURE', payload: error.message });
}
};
const logout = async () => {
try {
await authService.logout();
localStorage.removeItem('token');
dispatch({ type: 'LOGOUT' });
} catch (error) {
console.error('Logout error:', error);
}
};
const updateUser = (userData) => {
dispatch({ type: 'UPDATE_USER', payload: userData });
};
return (
<AuthContext.Provider value={{
...state,
login,
logout,
updateUser
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
この認証用のContextでは、以下の機能を提供しています:
- 自動ログイン:ページを開いた時の認証状態復元
- ログイン・ログアウト:認証処理
- ユーザー情報更新:プロフィール変更など
Redux Toolkitを使った構成
大規模プロジェクトでは、Redux Toolkitがおすすめです。
src/
├── store/
│ ├── index.js
│ ├── slices/
│ │ ├── authSlice.js
│ │ ├── userSlice.js
│ │ ├── notificationSlice.js
│ │ └── themeSlice.js
│ ├── middleware/
│ │ ├── authMiddleware.js
│ │ └── apiMiddleware.js
│ └── selectors/
│ ├── authSelectors.js
│ └── userSelectors.js
└── features/
├── auth/
│ ├── components/
│ └── hooks/
└── dashboard/
├── components/
└── hooks/
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import authSlice from './slices/authSlice';
import userSlice from './slices/userSlice';
import notificationSlice from './slices/notificationSlice';
import themeSlice from './slices/themeSlice';
export const store = configureStore({
reducer: {
auth: authSlice,
user: userSlice,
notification: notificationSlice,
theme: themeSlice
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
})
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Redux Toolkitを使うことで、以下のメリットがあります:
- 予測しやすい状態管理:決まったパターンで状態を変更
- 開発ツールが充実:Redux DevToolsで状態の変化を確認
- 時間旅行デバッグ:状態の変更履歴を辿れる
適切な状態管理の構成により、アプリケーションの複雑性を管理できます。 プロジェクトの規模に応じて、最適な手法を選んでくださいね。
スタイリングの構成
Reactアプリケーションのスタイリングも、適切な構成で管理することが大切です。
スタイリング手法の選択
プロジェクト規模別の推奨手法
規模 | 推奨手法 | 理由 |
---|---|---|
小規模 | CSS Modules | ファイルごとにスタイルを分離 |
中規模 | Styled Components | 動的なスタイリングが簡単 |
大規模 | CSS-in-JS + Design System | 一貫性のあるデザイン |
それぞれの特徴を見ていきましょう。
CSS Modulesを使った構成
基本的な構成
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ ├── Button.module.css
│ │ └── index.js
│ ├── Card/
│ │ ├── Card.jsx
│ │ ├── Card.module.css
│ │ └── index.js
│ └── Modal/
│ ├── Modal.jsx
│ ├── Modal.module.css
│ └── index.js
├── styles/
│ ├── globals.css
│ ├── variables.css
│ └── mixins.css
└── App.jsx
CSS変数を使った例
/* src/styles/variables.css */
:root {
/* Colors */
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--light-color: #f8f9fa;
--dark-color: #343a40;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 3rem;
/* Typography */
--font-family-base: 'Helvetica Neue', Arial, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* Borders */
--border-radius: 0.375rem;
--border-width: 1px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
このCSS変数を定義することで、アプリ全体で一貫したデザインが保てます。
/* src/components/Button/Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
font-family: var(--font-family-base);
font-size: var(--font-size-base);
font-weight: 500;
line-height: 1.5;
text-decoration: none;
border: var(--border-width) solid transparent;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease-in-out;
outline: none;
}
.button:focus {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Variants */
.primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.primary:hover:not(:disabled) {
background-color: #0056b3;
border-color: #0056b3;
}
.secondary {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
color: white;
}
.secondary:hover:not(:disabled) {
background-color: #545b62;
border-color: #545b62;
}
.outline {
background-color: transparent;
border-color: var(--primary-color);
color: var(--primary-color);
}
.outline:hover:not(:disabled) {
background-color: var(--primary-color);
color: white;
}
/* Sizes */
.small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
}
.large {
padding: var(--spacing-md) var(--spacing-lg);
font-size: var(--font-size-lg);
}
/* States */
.loading {
position: relative;
color: transparent;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 1rem;
height: 1rem;
margin: -0.5rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
この CSS では、以下の特徴があります:
- CSS変数を活用:一貫したデザイン
- バリエーション対応:primary、secondary、outlineの種類
- サイズ対応:small、medium、largeのサイズ
- 状態対応:loading、disabled の状態
// src/components/Button/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Button.module.css';
const Button = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
...props
}) => {
const className = [
styles.button,
styles[variant],
styles[size],
loading && styles.loading
].filter(Boolean).join(' ');
return (
<button
type={type}
className={className}
onClick={onClick}
disabled={disabled || loading}
{...props}
>
{children}
</button>
);
};
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary', 'outline']),
size: PropTypes.oneOf(['small', 'medium', 'large']),
disabled: PropTypes.bool,
loading: PropTypes.bool,
onClick: PropTypes.func,
type: PropTypes.oneOf(['button', 'submit', 'reset'])
};
export default Button;
このButtonコンポーネントでは、propsに応じて適切なCSSクラスを適用しています。
使う時はこんな感じ:
// いろんなパターンで使える
<Button variant="primary" size="large">
メインボタン
</Button>
<Button variant="outline" size="small" loading>
読み込み中
</Button>
<Button variant="secondary" disabled>
無効なボタン
</Button>
Styled Componentsを使った構成
中〜大規模プロジェクトでは、Styled Componentsもよく使われます。
// src/styles/theme.js
export const theme = {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a8a'
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827'
}
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '3rem'
},
typography: {
fontFamily: {
base: '"Helvetica Neue", Arial, sans-serif',
mono: '"SF Mono", Monaco, monospace'
},
fontSize: {
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem'
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700
}
},
borderRadius: {
sm: '0.125rem',
base: '0.375rem',
md: '0.5rem',
lg: '0.75rem',
full: '9999px'
},
shadows: {
sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
md: '0 4px 6px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px rgba(0, 0, 0, 0.1)'
}
};
このtheme
オブジェクトで、デザインの基準となる値を定義しています。
Styled Componentsを使うと、JSの中でCSSを書けるので、動的なスタイリングがとても簡単になります。
適切なスタイリング構成により、一貫性のあるUIと効率的な開発が可能になります。 プロジェクトの規模と要件に応じて、最適な手法を選んでくださいね。
テストファイルの配置
効率的なテスト戦略と適切なファイル配置は、Reactアプリの品質を保つために欠かせません。
テストファイルの配置パターン
パターン1: コンポーネントと同じディレクトリに配置
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ ├── Button.test.jsx
│ │ ├── Button.module.css
│ │ └── index.js
│ ├── UserCard/
│ │ ├── UserCard.jsx
│ │ ├── UserCard.test.jsx
│ │ ├── UserCard.module.css
│ │ └── index.js
│ └── Modal/
│ ├── Modal.jsx
│ ├── Modal.test.jsx
│ ├── Modal.module.css
│ └── index.js
├── pages/
│ ├── Home/
│ │ ├── Home.jsx
│ │ ├── Home.test.jsx
│ │ └── index.js
│ └── About/
│ ├── About.jsx
│ ├── About.test.jsx
│ └── index.js
└── hooks/
├── useAuth.js
└── useAuth.test.js
このパターンのメリット:
- 関連ファイルが近くにある:コンポーネントとテストが同じ場所
- ファイル移動が楽:コンポーネントを移動する時、テストも一緒に移動
- 実装とテストの同期が取りやすい:変更した時にテストも忘れずに更新
パターン2: 専用のテストディレクトリを作成
src/
├── components/
│ ├── Button/
│ ├── UserCard/
│ └── Modal/
├── pages/
│ ├── Home/
│ └── About/
├── hooks/
│ ├── useAuth.js
│ └── useApi.js
└── __tests__/
├── components/
│ ├── Button.test.jsx
│ ├── UserCard.test.jsx
│ └── Modal.test.jsx
├── pages/
│ ├── Home.test.jsx
│ └── About.test.jsx
├── hooks/
│ ├── useAuth.test.js
│ └── useApi.test.js
└── utils/
├── helpers.test.js
└── validation.test.js
このパターンのメリット:
- テストファイルが集約される:テスト関連のファイルが一箇所に
- 本番コードとテストコードが分離される:役割が明確に分かれる
- テストの実行が効率的:テストファイルを見つけやすい
テストファイルの実装例
コンポーネントのテスト
// src/components/Button/Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import Button from './Button';
import { theme } from '../../styles/theme';
// テストユーティリティ
const renderWithTheme = (component) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
describe('Button Component', () => {
describe('基本的な動作', () => {
it('正しくレンダリングされる', () => {
renderWithTheme(<Button>Click me</Button>);
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('クリックイベントが正しく発火する', () => {
const handleClick = jest.fn();
renderWithTheme(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('disabled状態の時はクリックできない', () => {
const handleClick = jest.fn();
renderWithTheme(
<Button disabled onClick={handleClick}>
Click me
</Button>
);
expect(screen.getByRole('button')).toBeDisabled();
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});
describe('Props による表示の変化', () => {
it('variantプロパティに応じて適切なスタイルが適用される', () => {
const { rerender } = renderWithTheme(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('primary');
rerender(
<ThemeProvider theme={theme}>
<Button variant="secondary">Secondary</Button>
</ThemeProvider>
);
expect(screen.getByRole('button')).toHaveClass('secondary');
});
it('loading状態の時は適切な表示がされる', () => {
renderWithTheme(<Button loading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('button')).toHaveClass('loading');
});
});
describe('アクセシビリティ', () => {
it('キーボードナビゲーションが機能する', () => {
const handleClick = jest.fn();
renderWithTheme(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveFocus();
fireEvent.keyDown(button, { key: 'Enter' });
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('適切なARIA属性が設定される', () => {
renderWithTheme(
<Button aria-label="Close modal" disabled>
×
</Button>
);
expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Close modal');
expect(screen.getByRole('button')).toHaveAttribute('disabled');
});
});
});
このテストでは、以下をチェックしています:
- 基本的な動作:正しく表示されるか、クリックできるか
- Props による変化:propsに応じて表示が変わるか
- アクセシビリティ:キーボード操作やARIA属性が正しいか
カスタムフックのテスト
// src/hooks/useAuth.test.js
import { renderHook, act } from '@testing-library/react';
import { useAuth } from './useAuth';
import { AuthProvider } from '../context/AuthContext';
// モックAPI
const mockApi = {
login: jest.fn(),
logout: jest.fn(),
getCurrentUser: jest.fn()
};
jest.mock('../services/authService', () => ({
authService: mockApi
}));
describe('useAuth Hook', () => {
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>;
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
});
describe('初期状態', () => {
it('初期状態が正しく設定される', () => {
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
describe('ログイン機能', () => {
it('ログイン成功時に状態が更新される', async () => {
const mockUser = { id: 1, name: 'Test User', email: 'test@example.com' };
const mockToken = 'mock-token';
mockApi.login.mockResolvedValue({
user: mockUser,
token: mockToken
});
const { result } = renderHook(() => useAuth(), { wrapper });
await act(async () => {
await result.current.login({
email: 'test@example.com',
password: 'password'
});
});
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
expect(localStorage.getItem('token')).toBe(mockToken);
});
it('ログイン失敗時にエラーが設定される', async () => {
const errorMessage = 'Invalid credentials';
mockApi.login.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useAuth(), { wrapper });
await act(async () => {
await result.current.login({
email: 'test@example.com',
password: 'wrong-password'
});
});
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(errorMessage);
});
});
});
カスタムフックのテストでは、以下をチェックしています:
- 初期状態:正しい初期値が設定されているか
- 成功ケース:正常な処理が動作するか
- エラーケース:エラーが適切に処理されるか
テスト設定ファイル
Jest設定
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx}',
'<rootDir>/src/**/*.{test,spec}.{js,jsx}'
],
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/serviceWorker.js',
'!src/**/*.stories.{js,jsx}'
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};
この設定では、以下を指定しています:
- testEnvironment:ブラウザ環境をシミュレート
- setupFilesAfterEnv:テスト実行前の設定
- moduleNameMapping:CSSファイルや画像ファイルのモック
- coverageThreshold:テストカバレッジの最低基準
テストユーティリティ
// src/test-utils/index.js
import React from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from '../context/AuthContext';
import { theme } from '../styles/theme';
// すべてのプロバイダーを含むカスタムレンダー
const AllTheProviders = ({ children }) => {
return (
<BrowserRouter>
<ThemeProvider theme={theme}>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
</BrowserRouter>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };
このユーティリティがあることで、テストを書く時に毎回プロバイダーをセットアップする必要がなくなります。
適切なテストファイルの配置と実装により、アプリケーションの品質を保てます。 テストは開発の最初から組み込んで、継続的に実行することが大切ですよ。
まとめ
Reactアプリケーションのディレクトリ構成について、詳しく解説してきました。
重要なポイント
プロジェクト規模に応じた構成選択
小規模なプロジェクトはシンプルな機能別構成から始めて、プロジェクトが成長するにつれて段階的に発展させていきましょう。 無理に複雑な構成にする必要はありません。
一貫性のある命名規則
- コンポーネント:PascalCase(UserProfile.jsx)
- ディレクトリ:kebab-case(user-profile/)
- ファイル:用途に応じた適切な命名
チーム内でルールを決めて、みんなで守ることが大切です。
保守性を考慮した設計
関連するファイルは近くに配置し、インデックスファイルを活用して使いやすくしましょう。 適切な責任分離により、変更の影響範囲を小さく保てます。
状態管理の適切な構成
- 小規模:useState + useContext
- 中規模:Context API + useReducer
- 大規模:Redux Toolkit
プロジェクトの規模に応じて、適切なツールを選択してください。
スタイリングの統一
CSS変数を使って一貫性のあるデザインシステムを構築しましょう。 再利用可能なコンポーネントにより、開発効率が向上します。
テストの戦略的配置
テストファイルは適切な場所に配置し、テストユーティリティを活用して効率的にテストを書きましょう。 継続的な品質管理が、長期的なプロジェクトの成功につながります。
実践的なアドバイス
構成の進化
最初は小さく始めて、プロジェクトの成長に応じて段階的に拡張していきましょう。 完璧な構成を最初から作る必要はありません。
チーム開発での工夫
明確なディレクトリ構成ルールを決めて、ドキュメント化しておきましょう。 新しいメンバーが参加した時に、すぐに理解できるようにすることが大切です。
メンテナンスのポイント
定期的にリファクタリングを行い、未使用ファイルの削除や依存関係の整理を忘れずに。 プロジェクトを常にきれいな状態に保ちましょう。
今日から始められること
まずは小さな改善から
既存のプロジェクトがある場合は、一度にすべてを変更する必要はありません。 少しずつ改善していくことで、リスクを最小限に抑えられます。
新しいプロジェクトでの活用
新しくプロジェクトを始める時は、この記事で紹介したパターンを参考にしてみてください。 プロジェクトの規模に応じて、適切な構成を選択しましょう。
チームでの話し合い
もしチームで開発している場合は、この記事を参考に構成について話し合ってみてください。 みんなで決めたルールは、きっと長く使えるものになりますよ。
適切なディレクトリ構成は、Reactアプリケーションの開発効率と保守性を大幅に向上させます。 この記事で紹介したパターンを参考に、あなたのプロジェクトに最適な構成を見つけてくださいね。
良いディレクトリ構成は、チーム全体の生産性向上につながる重要な投資です。 ぜひ、今日から実践してみてください!