第11章: API 通信
ほとんどの Web アプリケーションは、サーバーからデータを取得したり、ユーザーの入力をサーバーに送信したりします。
この章では、React で API 通信を行う方法を解説します。
fetch API と async/await を使ったデータ取得、
ローディング・エラー状態の管理、そして Tomcat バックエンドとの連携時の注意点を学びます。
API 通信の基本
React はあくまで UI ライブラリであり、API 通信の仕組みは含まれていません。
ブラウザ標準の fetch API を使ってサーバーと通信します。
典型的な API 通信の流れは以下の通りです。
- コンポーネントがマウントされる(画面に表示される)
useEffect内で API リクエストを送信する- レスポンスを受け取り、State にデータを格納する
- State の更新によりコンポーネントが再レンダーされ、データが表示される
fetch API
fetch はブラウザに組み込まれた HTTP リクエスト用の関数です。
Promise を返すので、async/await と組み合わせて使用します。
// GET リクエスト
const response = await fetch('/api/users');
const data = await response.json();
// POST リクエスト
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '田中太郎' }),
});
fetch は HTTP エラー(404, 500 など)で例外をスローしません。
ネットワークエラー(サーバーに接続できないなど)の場合のみ例外が発生します。
HTTP エラーを検知するには、response.ok(ステータスコード 200-299 で true)を
確認する必要があります。
async function fetchData(url) {
const response = await fetch(url);
// HTTP エラーチェック(fetch は自動で throw しない)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
useEffect でのデータ取得
コンポーネントのマウント時にデータを取得する基本パターンです。
useEffect の中で async 関数を定義して呼び出します。
useEffect のコールバック関数自体を async にしてはいけません。
useEffect はクリーンアップ関数(または undefined)の返却を期待しますが、
async 関数は Promise を返すため、React が正しくクリーンアップを処理できません。
代わりに、内部で async 関数を定義して即座に呼び出します。
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// NG: useEffect のコールバック自体を async にしない
// useEffect(async () => { ... }) ← これはダメ
// OK: 内部で async 関数を定義して呼び出す
async function fetchUsers() {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []); // 空配列: マウント時に 1 回だけ実行
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
ローディングとエラー処理
実際のアプリケーションでは、データの読み込み中やエラー発生時に適切な UI を表示する必要があります。
loading と error の State を追加して管理しましょう。
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`サーバーエラー: ${response.status}`);
}
const data = await response.json();
if (!ignore) {
setUsers(data);
}
} catch (err) {
if (!ignore) {
setError(err.message);
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
fetchUsers();
return () => {
ignore = true;
};
}, []);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラーが発生しました: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
ignore フラグによるレースコンディション対策:
コンポーネントがアンマウントされた後や、依存値が変わった後に古いレスポンスが
State にセットされるのを防ぎます。これは React 公式ドキュメントでも推奨されている定番パターンです。
POST リクエスト
データの作成や更新には POST / PUT / PATCH リクエストを使用します。 フォーム送信のタイミングで API を呼び出し、結果に応じて UI を更新します。
import { useState } from 'react';
function CreateUser() {
const [name, setName] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error(`送信に失敗しました: ${response.status}`);
}
const newUser = await response.json();
console.log('作成されたユーザー:', newUser);
setName(''); // フォームをリセット
} catch (err) {
setError(err.message);
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="ユーザー名"
disabled={submitting}
/>
<button type="submit" disabled={submitting || !name.trim()}>
{submitting ? '作成中...' : '作成'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}
CRUD 操作の実装
実際のアプリケーションでは、データの作成 (Create)、読み取り (Read)、 更新 (Update)、削除 (Delete) を組み合わせて実装します。
| 操作 | HTTP メソッド | URL 例 | 説明 |
|---|---|---|---|
| 一覧取得 | GET | /api/users | 全ユーザーの取得 |
| 詳細取得 | GET | /api/users/1 | ID=1 のユーザーを取得 |
| 作成 | POST | /api/users | 新規ユーザーの作成 |
| 更新 | PUT / PATCH | /api/users/1 | ID=1 のユーザーを更新 |
| 削除 | DELETE | /api/users/1 | ID=1 のユーザーを削除 |
import { useState, useEffect } from 'react';
const API_URL = '/api/users';
function UserManager() {
const [users, setUsers] = useState([]);
const [newName, setNewName] = useState('');
const [loading, setLoading] = useState(true);
// Read: 一覧取得
useEffect(() => {
fetch(API_URL)
.then(res => res.json())
.then(data => setUsers(data))
.finally(() => setLoading(false));
}, []);
// Create: 新規作成
async function handleCreate() {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName }),
});
const created = await response.json();
setUsers(prev => [...prev, created]);
setNewName('');
}
// Delete: 削除
async function handleDelete(id) {
await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
setUsers(prev => prev.filter(user => user.id !== id));
}
if (loading) return <p>読み込み中...</p>;
return (
<div>
<h2>ユーザー管理</h2>
<div>
<input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="新しいユーザー名"
/>
<button onClick={handleCreate} disabled={!newName.trim()}>追加</button>
</div>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => handleDelete(user.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
CORS とプロキシ設定
開発時、React の開発サーバー(Vite: localhost:5173)と
バックエンドの API サーバー(Tomcat: localhost:8080)は
ポートが異なるため、ブラウザの同一オリジンポリシーにより
リクエストがブロックされます。これが CORS(Cross-Origin Resource Sharing)の問題です。
解決策 1: Vite のプロキシ設定(開発時推奨)
Vite の設定でプロキシを定義すると、開発サーバーが API リクエストを代理して転送します。 ブラウザから見ると同一オリジンへのリクエストになるため、CORS の問題が発生しません。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// /api で始まるリクエストを Tomcat に転送
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
この設定により、アプリケーションコード内で fetch('/api/users') と書くだけで、
開発時は Vite が http://localhost:8080/api/users にリクエストを転送してくれます。
本番環境(Tomcat に直接デプロイ)では同一オリジンになるため、プロキシは不要です。
解決策 2: バックエンド側で CORS ヘッダーを設定
Tomcat のバックエンドで CORS ヘッダーを設定する方法もあります。
web.xml で Tomcat 標準の CORS フィルターを有効にします。
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>http://localhost:5173</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,Authorization</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
開発時は Vite のプロキシ設定が手軽でおすすめです。 本番環境では React と API が同一の Tomcat 上で動作することが多く、 CORS は問題になりません。CORS フィルターは API を別ドメインで公開する場合に使用します。
カスタムフックで共通化
API 通信のパターン(loading / error / data の管理)は繰り返し使うため、 カスタムフックに切り出すと便利です。
import { useState, useCallback } from 'react';
function useApi() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (url, options = {}) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, []);
return { request, loading, error };
}
export default useApi;
import useApi from './hooks/useApi';
function UserPage() {
const { request, loading, error } = useApi();
const [users, setUsers] = useState([]);
useEffect(() => {
request('/api/users')
.then(data => setUsers(data))
.catch(() => {}); // エラーは useApi 内で管理
}, [request]);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
ベストプラクティス
| 項目 | 推奨事項 |
|---|---|
| ローディング表示 | データ取得中は必ずローディング UI を表示し、ユーザーに待機状態を伝える |
| エラーハンドリング | response.ok を確認し、ネットワークエラーと HTTP エラーの両方を処理する |
| レースコンディション | ignore フラグやAbortController で古いリクエストの結果を破棄する |
| 二重送信の防止 | submitting State でボタンを disabled にし、連続クリックを防ぐ |
| API URL の管理 | 環境変数(import.meta.env.VITE_API_URL)でベース URL を管理する |
| 共通化 | API 通信のロジックはカスタムフックや API クライアントモジュールに集約する |
本番環境で React(フロントエンド)と Java(バックエンド)を同じ Tomcat にデプロイする場合、
API のパスは相対パス(/api/users)で記述するのが一般的です。
これにより、CORS の問題を回避でき、環境ごとの設定変更が最小限になります。