Contents
Reactとは
React(React.js)は、Facebookが開発したJavaScriptライブラリで、主にユーザーインターフェース(UI)の構築に使用されます。特に、複雑で動的なWebアプリケーションの開発において、コンポーネントベースのアーキテクチャを提供し、効率的で保守性の高いコードの作成を可能にします。
Reactは「ライブラリ」として分類されますが、その豊富なエコシステムにより、実質的にはフルスタックフレームワークとして機能することができます。
Reactの歴史
2013年:React誕生
- Facebook(現Meta)のJordan Walkeによって開発
- 社内でのFacebookとInstagramの開発ニーズから生まれる
- 初のオープンソース公開
2014年:コミュニティの拡大
- 開発者コミュニティが急速に成長
- 企業での採用が増加
2015年:React Native登場
- モバイルアプリ開発への展開
- “Learn once, write anywhere”の理念を実現
2016年:React 15
- 大幅な内部リファクタリング
- パフォーマンスの向上
2017年:React 16(React Fiber)
- 完全に書き直された新しいアーキテクチャ
- 非同期レンダリングの基盤
2018年:React Hooks導入
- 関数コンポーネントでの状態管理が可能に
- クラスコンポーネントの代替として登場
2020年〜現在:継続的な進化
- Concurrent Mode(現在のConcurrent Features)
- React 18でのAutomatic Batching
- Server Components(実験的機能)
Reactの特徴
1. コンポーネントベースアーキテクチャ
Reactでは、UIを独立したコンポーネントに分割して構築します。これにより、再利用性が高く、保守しやすいコードが書けます。
2. 仮想DOM(Virtual DOM)
Reactは仮想DOMという仕組みを使用して、実際のDOMの操作を最小限に抑え、パフォーマンスを向上させます。
3. 宣言的プログラミング
Reactでは、「どのように」ではなく「何を」表示するかを記述します。これにより、コードの可読性と予測可能性が向上します。
4. 単方向データフロー
データは親コンポーネントから子コンポーネントへ一方向に流れます。これにより、アプリケーションの状態管理が簡単になります。
5. JSX(JavaScript XML)
JavaScriptの中でHTMLライクな記法を使用できます。これにより、UIの構造を直感的に記述できます。
6. 豊富なエコシステム
React Router、Redux、Material-UIなど、多くのライブラリやツールがReactをサポートしています。
基本概念
コンポーネント
Reactアプリケーションの基本単位。関数コンポーネントとクラスコンポーネントの2種類があります。
// 関数コンポーネント
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
// クラスコンポーネント
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
Props
コンポーネントに渡される読み取り専用のデータです。
function UserProfile({ name, age, email }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Email: {email}</p>
</div>
);
}
State
コンポーネント内で管理される変更可能なデータです。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
JSX
JavaScriptの中でHTMLライクな記法を使用できる構文拡張です。
const element = <h1>Hello, world!</h1>;
const user = { name: 'John', age: 30 };
const userElement = <h1>Hello, {user.name}!</h1>;
環境構築と基本的な使用方法
1. 環境構築
Create React Appを使用
npx create-react-app my-app
cd my-app
npm start
Viteを使用(推奨)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
2. 基本的なファイル構成
my-app/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ ├── App.js
│ ├── App.css
│ └── index.js
├── package.json
└── README.md
3. 基本的なコンポーネント作成
// src/components/Hello.js
import React from 'react';
function Hello({ name }) {
return (
<div>
<h1>Hello, {name}!</h1>
</div>
);
}
export default Hello;
// src/App.js
import React from 'react';
import Hello from './components/Hello';
import './App.css';
function App() {
return (
<div className="App">
<Hello name="World" />
</div>
);
}
export default App;
React Hooks
React Hooksは、関数コンポーネントで状態管理やライフサイクルメソッドを使用できるようにする機能です。
useState
コンポーネントの状態を管理します。
import React, { useState } from 'react';
function TextInput() {
const [text, setText] = useState('');
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>You typed: {text}</p>
</div>
);
}
useEffect
副作用を処理します(APIコール、DOM操作など)
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// APIからユーザーデータを取得
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []); // 空の依存配列:コンポーネントマウント時のみ実行
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useContext
コンテキストを使用してコンポーネント間でデータを共有します。
import React, { createContext, useContext, useState } from 'react';
// コンテキスト作成
const ThemeContext = createContext();
// プロバイダーコンポーネント
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// コンテキストを使用するコンポーネント
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Toggle Theme
</button>
);
}
カスタムHooks
独自のHooksを作成して、ロジックを再利用できます。
import { useState, useEffect } from 'react';
// カスタムHook: APIからデータを取得
function useFetch(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('Network response was not ok');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 使用例
function PostList() {
const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
状態管理
1. ローカル状態管理
小規模なアプリケーションでは、useStateとuseContextで十分です。
2. Redux
大規模なアプリケーションでの状態管理によく使用されます。
npm install @reduxjs/toolkit react-redux
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
3. Zustand
軽量で使いやすい状態管理ライブラリです。
npm install zustand
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Reactエコシステムとフレームワーク
1. Next.js
本格的なReactフレームワークで、SSR(Server-Side Rendering)、SSG(Static Site Generation)をサポートします。
npx create-next-app@latest my-next-app
特徴:
- ファイルベースルーティング
- 自動コード分割
- 画像最適化
- APIルート
2. Gatsby
静的サイト生成に特化したフレームワークです。
npm install -g gatsby-cli
gatsby new my-gatsby-site
特徴:
- GraphQLデータレイヤー
- 豊富なプラグインエコシステム
- パフォーマンス最適化
3. Remix
Web標準に基づいたフルスタックReactフレームワークです。
npx create-remix@latest
特徴:
- ネストされたルーティング
- データローダー
- エラーハンドリング
4. React Router
SPAでのクライアントサイドルーティングを提供します。
npm install react-router-dom
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
5. UIライブラリ
Material-UI (MUI)
npm install @mui/material @emotion/react @emotion/styled
Ant Design
npm install antd
Chakra UI
npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion
実践的なコーディング例
Todo アプリケーション
完全な機能を持つTodoアプリの例です。
import React, { useState, useEffect } from 'react';
import './TodoApp.css';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [filter, setFilter] = useState('all');
// ローカルストレージから読み込み
useEffect(() => {
const saved = localStorage.getItem('todos');
if (saved) {
setTodos(JSON.parse(saved));
}
}, []);
// ローカルストレージに保存
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue,
completed: false,
createdAt: new Date().toISOString()
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const editTodo = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
return (
<div className="todo-app">
<h1>Todo App</h1>
<div className="input-section">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add a new todo..."
/>
<button onClick={addTodo}>Add</button>
</div>
<div className="filter-section">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All ({todos.length})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active ({todos.filter(t => !t.completed).length})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed ({todos.filter(t => t.completed).length})
</button>
</div>
<div className="todo-list">
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
onDelete={() => deleteTodo(todo.id)}
onEdit={(newText) => editTodo(todo.id, newText)}
/>
))}
</div>
</div>
);
}
function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleSave = () => {
if (editText.trim()) {
onEdit(editText);
setIsEditing(false);
}
};
const handleCancel = () => {
setEditText(todo.text);
setIsEditing(false);
};
return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
{isEditing ? (
<div className="edit-mode">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') handleCancel();
}}
autoFocus
/>
<button onClick={handleSave}>Save</button>
<button onClick={handleCancel}>Cancel</button>
</div>
) : (
<div className="view-mode">
<span className="todo-text">{todo.text}</span>
<div className="todo-actions">
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={onDelete}>Delete</button>
</div>
</div>
)}
</div>
);
}
export default TodoApp;
データフェッチングとエラーハンドリング
import React, { useState, useEffect } from 'react';
function UserManagement() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedUser, setSelectedUser] = useState(null);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const deleteUser = async (userId) => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete user');
setUsers(users.filter(user => user.id !== userId));
} catch (err) {
setError(err.message);
}
};
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} onRetry={fetchUsers} />;
return (
<div className="user-management">
<h1>User Management</h1>
<div className="user-grid">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onSelect={setSelectedUser}
onDelete={deleteUser}
/>
))}
</div>
{selectedUser && (
<UserModal
user={selectedUser}
onClose={() => setSelectedUser(null)}
/>
)}
</div>
);
}
function LoadingSpinner() {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading users...</p>
</div>
);
}
function ErrorMessage({ error, onRetry }) {
return (
<div className="error-message">
<h2>Error</h2>
<p>{error}</p>
<button onClick={onRetry}>Retry</button>
</div>
);
}
function UserCard({ user, onSelect, onDelete }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>{user.company.name}</p>
<div className="user-actions">
<button onClick={() => onSelect(user)}>View Details</button>
<button
onClick={() => onDelete(user.id)}
className="delete-btn"
>
Delete
</button>
</div>
</div>
);
}
function UserModal({ user, onClose }) {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>{user.name}</h2>
<div className="user-details">
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Phone:</strong> {user.phone}</p>
<p><strong>Website:</strong> {user.website}</p>
<p><strong>Company:</strong> {user.company.name}</p>
<p><strong>Address:</strong> {user.address.street}, {user.address.city}</p>
</div>
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
パフォーマンス最適化
1. React.memo
コンポーネントの不要な再レンダリングを防ぎます。
import React, { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
console.log('ExpensiveComponent rendered');
return (
<div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<button onClick={onClick}>Click me</button>
</div>
);
});
// propsの比較をカスタマイズ
const CustomMemoComponent = memo(function CustomMemoComponent({ user }) {
return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});
2. useMemo
重い計算結果をメモ化します。
import React, { useMemo } from 'react';
function ExpensiveCalculation({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.category === filter && item.price > 100
);
}, [items, filter]);
const total = useMemo(() => {
console.log('Calculating total...');
return filteredItems.reduce((sum, item) => sum + item.price, 0);
}, [filteredItems]);
return (
<div>
<p>Items: {filteredItems.length}</p>
<p>Total: ${total}</p>
</div>
);
}
3. useCallback
関数をメモ化して、子コンポーネントの不要な再レンダリングを防ぎます。
import React, { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleNameChange = useCallback((newName) => {
setName(newName);
}, []);
return (
<div>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
<NameInput onNameChange={handleNameChange} />
</div>
);
}
const ChildComponent = memo(function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
4. コード分割
React.lazyとSuspenseを使用して、コンポーネントを遅延読み込みします。
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const AnotherLazyComponent = lazy(() => import('./AnotherLazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
<AnotherLazyComponent />
</Suspense>
</div>
);
}
テスト
1. React Testing Library
Reactコンポーネントのテストによく使用されます。
npm install --save-dev @testing-library/react @testing-library/jest-dom
// Counter.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Counter from './Counter';
describe('Counter', () => {
test('renders initial count', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('increments count when button is clicked', () => {
render(<Counter />);
const button = screen.getByText('Increment');
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements count when decrement button is clicked', () => {
render(<Counter initialCount={5} />);
const button = screen.getByText('Decrement');
fireEvent.click(button);
expect(screen.getByText('Count: 4')).toBeInTheDocument();
});
});
2. カスタムフックのテスト
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('should increment count', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
3. モックとスタブ
// UserList.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
// fetchをモック
global.fetch = jest.fn();
describe('UserList', () => {
beforeEach(() => {
fetch.mockClear();
});
test('displays loading state initially', () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => []
});
render(<UserList />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('displays users after successful fetch', async () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUsers
});
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
test('displays error message on fetch failure', async () => {
fetch.mockRejectedValueOnce(new Error('API Error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
4. E2Eテスト(Cypress)
npm install --save-dev cypress
// cypress/integration/todo.spec.js
describe('Todo App', () => {
beforeEach(() => {
cy.visit('/');
});
it('should add a new todo', () => {
cy.get('[data-testid=todo-input]').type('Learn Cypress');
cy.get('[data-testid=add-button]').click();
cy.get('[data-testid=todo-list]').should('contain', 'Learn Cypress');
});
it('should mark todo as completed', () => {
cy.get('[data-testid=todo-input]').type('Complete this task');
cy.get('[data-testid=add-button]').click();
cy.get('[data-testid=todo-checkbox]').first().click();
cy.get('[data-testid=todo-item]').first().should('have.class', 'completed');
});
it('should delete a todo', () => {
cy.get('[data-testid=todo-input]').type('Delete me');
cy.get('[data-testid=add-button]').click();
cy.get('[data-testid=delete-button]').first().click();
cy.get('[data-testid=todo-list]').should('not.contain', 'Delete me');
});
});
最新のReact機能とベストプラクティス
1. Server Components(実験的機能)
// app/page.js (Next.js 13+ App Router)
import { Suspense } from 'react';
import UserList from './UserList';
import LoadingSkeleton from './LoadingSkeleton';
// これはServer Componentです
export default async function HomePage() {
return (
<div>
<h1>Welcome to our App</h1>
<Suspense fallback={<LoadingSkeleton />}>
<UserList />
</Suspense>
</div>
);
}
// Server Component - サーバーサイドでデータフェッチ
async function UserList() {
const users = await fetch('https://api.example.com/users').then(res => res.json());
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
2. Concurrent Features
import { startTransition, useDeferredValue, useTransition } from 'react';
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
const deferredQuery = useDeferredValue(query);
useEffect(() => {
if (deferredQuery) {
startTransition(() => {
// 重い検索処理
searchAPI(deferredQuery).then(setResults);
});
}
}, [deferredQuery]);
return (
<div>
{isPending && <div>Searching...</div>}
<ResultsList results={results} />
</div>
);
}
3. Error Boundaries
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// エラーログサービスに送信
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// 使用例
function App() {
return (
<ErrorBoundary>
<Header />
<MainContent />
<Footer />
</ErrorBoundary>
);
}
4. TypeScriptとの統合
// types.ts
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
export interface TodoItem {
id: number;
text: string;
completed: boolean;
createdAt: string;
}
// UserProfile.tsx
import React, { FC } from 'react';
import { User } from './types';
interface UserProfileProps {
user: User;
onEdit: (user: User) => void;
onDelete: (id: number) => void;
}
const UserProfile: FC<UserProfileProps> = ({ user, onEdit, onDelete }) => {
const handleEdit = () => {
onEdit({ ...user, name: 'Updated Name' });
};
return (
<div className="user-profile">
<img src={user.avatar || '/default-avatar.png'} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={handleEdit}>Edit</button>
<button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
};
export default UserProfile;
// カスタムHookの型定義
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
const fetchData = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const data: T = await response.json();
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
fetchData();
}, [url]);
return state;
}
実際のプロジェクトでの設計パターン
1. コンポーネント設計パターン
Compound Components
// Tabs.js
import React, { createContext, useContext, useState } from 'react';
const TabsContext = createContext();
function Tabs({ children, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ index, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={`tab ${activeTab === index ? 'active' : ''}`}
onClick={() => setActiveTab(index)}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ index, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== index) return null;
return <div className="tab-panel">{children}</div>;
}
// 使用例
function App() {
return (
<Tabs defaultTab={0}>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
<Tab index={2}>Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel index={0}>Content 1</TabPanel>
<TabPanel index={1}>Content 2</TabPanel>
<TabPanel index={2}>Content 3</TabPanel>
</TabPanels>
</Tabs>
);
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
export default Tabs;
Render Props パターン
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
}
// 使用例
function UserList() {
return (
<DataFetcher
url="https://api.example.com/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
/>
);
}
2. フォーム管理
React Hook Form
npm install react-hook-form
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
function ContactForm() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
reset
} = useForm({
defaultValues: {
name: '',
email: '',
message: '',
category: 'general'
}
});
const onSubmit = async (data) => {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
alert('Message sent successfully!');
reset();
}
} catch (error) {
alert('Failed to send message');
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
{...register('name', {
required: 'Name is required',
minLength: { value: 2, message: 'Name must be at least 2 characters' }
})}
/>
{errors.name && <span className="error">{errors.name.message}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div className="form-group">
<label htmlFor="category">Category</label>
<Controller
name="category"
control={control}
render={({ field }) => (
<select {...field}>
<option value="general">General</option>
<option value="support">Support</option>
<option value="sales">Sales</option>
</select>
)}
/>
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
rows={5}
{...register('message', {
required: 'Message is required',
minLength: { value: 10, message: 'Message must be at least 10 characters' }
})}
/>
{errors.message && <span className="error">{errors.message.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}
3. 認証とルート保護
// AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// トークンをチェックしてユーザー情報を取得
const token = localStorage.getItem('token');
if (token) {
fetchUserProfile(token)
.then(setUser)
.catch(() => localStorage.removeItem('token'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const { token, user } = await response.json();
localStorage.setItem('token', token);
setUser(user);
return { success: true };
} else {
const { error } = await response.json();
return { success: false, error };
}
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
const value = {
user,
login,
logout,
isAuthenticated: !!user
};
if (loading) return <div>Loading...</div>;
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// ProtectedRoute.js
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// App.js での使用例
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import ProtectedRoute from './ProtectedRoute';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
パフォーマンス監視とデバッグ
1. React Developer Tools の活用
React Developer Toolsは、Reactアプリケーションのデバッグに不可欠なツールです。
- コンポーネントツリーの表示
- Props と State の確認
- パフォーマンスプロファイリング
- Hooks の状態確認
2. パフォーマンス測定
// Performance measurement
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Google Analytics or other analytics service
console.log(metric);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
// React Profiler API
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
console.log('Profiler', {
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<Main />
<Footer />
</Profiler>
);
}
3. デバッグ用カスタムフック
// useDebugValue for custom hooks
import { useDebugValue, useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// React DevTools で表示される
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
// Why-did-you-render for debugging unnecessary re-renders
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
まとめ
Reactは現代のWeb開発において中心的な役割を果たすライブラリです。その特徴と利点を整理すると:
Reactの主な強み
- コンポーネントベース設計:再利用可能で保守しやすいコード
- 仮想DOM:高いパフォーマンスと効率的な更新
- 豊富なエコシステム:多様なライブラリとツールのサポート
- 大きなコミュニティ:活発な開発とサポート
- 企業での採用:Facebook、Netflix、Airbnbなど多数の企業で使用
学習のロードマップ
- 基礎:JSX、コンポーネント、Props、State
- React Hooks:useState、useEffect、useContext
- 状態管理:Context API、Redux、Zustand
- ルーティング:React Router
- フォーム管理:React Hook Form
- テスト:React Testing Library、Jest
- パフォーマンス最適化:memo、useMemo、useCallback
- フレームワーク:Next.js、Gatsby、Remix
ベストプラクティス
- 関数コンポーネントとHooksを優先使用
- PropTypesやTypeScriptで型安全性を確保
- ESLintとPrettierでコード品質を維持
- テストを書いて品質を保証
- パフォーマンスを意識した開発
- アクセシビリティへの配慮
Reactは継続的に進化しており、Server Components、Concurrent Features、React 18の新機能など、常に新しい技術が導入されています。基礎をしっかり学んだ上で、最新の動向をキャッチアップしていくことが重要です。
このガイドを参考に、Reactの学習を進めて、現代的で効率的なWebアプリケーションの開発を始めてください。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント