
React演習問題(JSX・コンポーネント・Props・State)
初級問題(9問) JSXの書き方 <div class="container"> <h1 id="title&q […]
前回までで、Express.jsとPrismaを使用した本格的なバックエンドAPIの構築が完了しました。今回は、ReactフロントエンドからこのAPIに接続し、データを取得・操作する方法を学びます。現代のWeb開発では、Fetch APIとAxiosの2つの主要な方法でHTTP通信を行います。
npx create-react-app todo-frontend
cd todo-frontend
npm start
npm install axios
// src/api/client.js
import axios from 'axios';
// APIのベースURL
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';
// Axiosインスタンスの作成
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// リクエストインターセプター(後で認証トークンを追加します)
apiClient.interceptors.request.use(
(config) => {
// ここでリクエスト前の処理を追加
console.log('リクエスト送信:', config.method, config.url);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// レスポンスインターセプター
apiClient.interceptors.response.use(
(response) => {
console.log('レスポンス受信:', response.status, response.config.url);
return response;
},
(error) => {
console.error('APIエラー:', error.response?.status, error.message);
// エラーハンドリング
if (error.response?.status === 401) {
// 未認証エラー(後で処理を追加)
console.log('認証が必要です');
} else if (error.response?.status === 404) {
console.log('リソースが見つかりません');
}
return Promise.reject(error);
}
);
export default apiClient;
// src/api/todoService.js
import apiClient from './client';
// TODO一覧の取得
export const fetchTodos = async () => {
try {
const response = await apiClient.get('/todos');
return response.data;
} catch (error) {
console.error('TODO一覧の取得に失敗しました:', error);
throw error;
}
};
// 特定のTODOの取得
export const fetchTodoById = async (id) => {
try {
const response = await apiClient.get(`/todos/${id}`);
return response.data;
} catch (error) {
console.error(`TODO(ID: ${id})の取得に失敗しました:`, error);
throw error;
}
};
// TODOの新規作成
export const createTodo = async (todoData) => {
try {
const response = await apiClient.post('/todos', todoData);
return response.data;
} catch (error) {
console.error('TODOの作成に失敗しました:', error);
throw error;
}
};
// TODOの更新
export const updateTodo = async (id, todoData) => {
try {
const response = await apiClient.put(`/todos/${id}`, todoData);
return response.data;
} catch (error) {
console.error(`TODO(ID: ${id})の更新に失敗しました:`, error);
throw error;
}
};
// TODOの削除
export const deleteTodo = async (id) => {
try {
await apiClient.delete(`/todos/${id}`);
} catch (error) {
console.error(`TODO(ID: ${id})の削除に失敗しました:`, error);
throw error;
}
};
// 統計情報の取得
export const fetchTodoStats = async () => {
try {
const response = await apiClient.get('/todos/stats');
return response.data;
} catch (error) {
console.error('統計情報の取得に失敗しました:', error);
throw error;
}
};
// ページネーション付きTODO一覧の取得
export const fetchPaginatedTodos = async (page = 1, limit = 10) => {
try {
const response = await apiClient.get(`/todos/paginated?page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('ページネーション付きTODO一覧の取得に失敗しました:', error);
throw error;
}
};
Axiosだけでなく、ネイティブのFetch APIを使用する方法も学びましょう。
// src/api/todoServiceFetch.js
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';
// 汎用HTTPリクエスト関数
const request = async (endpoint, options = {}) => {
const url = `${API_BASE_URL}${endpoint}`;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
const config = { ...defaultOptions, ...options };
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 204 No Contentの場合はデータをパースしない
if (response.status === 204) {
return null;
}
const data = await response.json();
return data;
} catch (error) {
console.error('APIリクエストエラー:', error);
throw error;
}
};
// TODO一覧の取得(Fetch API版)
export const fetchTodosWithFetch = async () => {
return request('/todos');
};
// TODOの新規作成(Fetch API版)
export const createTodoWithFetch = async (todoData) => {
return request('/todos', {
method: 'POST',
body: JSON.stringify(todoData),
});
};
// TODOの更新(Fetch API版)
export const updateTodoWithFetch = async (id, todoData) => {
return request(`/todos/${id}`, {
method: 'PUT',
body: JSON.stringify(todoData),
});
};
// TODOの削除(Fetch API版)
export const deleteTodoWithFetch = async (id) => {
return request(`/todos/${id}`, {
method: 'DELETE',
});
};
Reactのカスタムフックを使用して、API通信のロジックを再利用可能にします。
// src/hooks/useTodos.js
import { useState, useEffect } from 'react';
import {
fetchTodos,
createTodo,
updateTodo,
deleteTodo,
fetchTodoStats
} from '../api/todoService';
export const useTodos = () => {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [stats, setStats] = useState(null);
// TODO一覧の取得
const getTodos = async () => {
setLoading(true);
setError(null);
try {
const data = await fetchTodos();
setTodos(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// 統計情報の取得
const getStats = async () => {
try {
const data = await fetchTodoStats();
setStats(data);
} catch (err) {
console.error('統計情報の取得に失敗しました:', err);
}
};
// TODOの新規作成
const addTodo = async (title) => {
try {
const newTodo = await createTodo({ title, completed: false });
setTodos(prev => [...prev, newTodo]);
return newTodo;
} catch (err) {
setError(err.message);
throw err;
}
};
// TODOの更新
const toggleTodo = async (id, completed) => {
try {
const updatedTodo = await updateTodo(id, { completed });
setTodos(prev =>
prev.map(todo => todo.id === id ? updatedTodo : todo)
);
return updatedTodo;
} catch (err) {
setError(err.message);
throw err;
}
};
// TODOの削除
const removeTodo = async (id) => {
try {
await deleteTodo(id);
setTodos(prev => prev.filter(todo => todo.id !== id));
} catch (err) {
setError(err.message);
throw err;
}
};
// コンポーネントマウント時にデータ取得
useEffect(() => {
getTodos();
getStats();
}, []);
return {
todos,
stats,
loading,
error,
addTodo,
toggleTodo,
removeTodo,
refreshTodos: getTodos,
refreshStats: getStats,
};
};
// src/components/TodoList.js
import React, { useState } from 'react';
import { useTodos } from '../hooks/useTodos';
const TodoList = () => {
const { todos, stats, loading, error, addTodo, toggleTodo, removeTodo } = useTodos();
const [newTodoTitle, setNewTodoTitle] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!newTodoTitle.trim()) return;
try {
await addTodo(newTodoTitle);
setNewTodoTitle('');
} catch (err) {
// エラー処理はuseTodos内で行われます
}
};
const handleToggle = async (id, completed) => {
try {
await toggleTodo(id, !completed);
} catch (err) {
// エラー処理
}
};
const handleDelete = async (id) => {
try {
await removeTodo(id);
} catch (err) {
// エラー処理
}
};
if (loading && todos.length === 0) {
return <div className="loading">読み込み中...</div>;
}
if (error) {
return <div className="error">エラー: {error}</div>;
}
return (
<div className="todo-app">
<h1>TODOアプリ</h1>
{stats && (
<div className="stats">
<p>総数: {stats.total} | 完了: {stats.completed} | 未完了: {stats.pending}</p>
</div>
)}
<form onSubmit={handleSubmit} className="todo-form">
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="新しいTODOを入力..."
className="todo-input"
/>
<button type="submit" className="add-button">追加</button>
</form>
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id, todo.completed)}
className="todo-checkbox"
/>
<span className="todo-title">{todo.title}</span>
<button
onClick={() => handleDelete(todo.id)}
className="delete-button"
>
削除
</button>
</li>
))}
</ul>
{todos.length === 0 && !loading && (
<p className="empty-message">TODOがありません</p>
)}
</div>
);
};
export default TodoList;
より堅牢なエラーハンドリングを実装します。
// src/utils/errorHandler.js
export class ApiError extends Error {
constructor(message, statusCode, details = null) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.details = details;
}
}
export const handleApiError = (error) => {
if (error.response) {
// サーバーからのエラーレスポンス
throw new ApiError(
error.response.data?.error || 'サーバーエラーが発生しました',
error.response.status,
error.response.data
);
} else if (error.request) {
// リクエストは送信されたがレスポンスがない
throw new ApiError('サーバーとの接続に失敗しました', 0);
} else {
// リクエスト設定中のエラー
throw new ApiError(error.message, 0);
}
};
// サービス層での使用例
export const enhancedFetchTodos = async () => {
try {
const response = await apiClient.get('/todos');
return response.data;
} catch (error) {
throw handleApiError(error);
}
};
Fetch APIとAxiosを使用したフロントエンドとバックエンドの連携が完成しました。次回は、JWT認証を実装して、ユーザーごとにTODOを管理できるようにします。これにより、アプリケーションのセキュリティと個人化が向上します。
今回学んだ内容:
これらの技術を組み合わせることで、ReactアプリケーションからバックエンドAPIとシームレスに連携する方法を習得しました。次回はセキュリティを強化する認証機能を実装します。