sample.js
import React, { useState, useEffect } from 'react';
function ApiExample() {
// 投稿一覧の状態
const [posts, setPosts] = useState([]);
const [loadingPosts, setLoadingPosts] = useState(true);
const [error, setError] = useState(null);
// 新規投稿の状態
const [newPost, setNewPost] = useState({ title: '', body: '' });
const [submitting, setSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
// コンポーネントマウント時に投稿一覧を取得
useEffect(() => {
fetchPosts();
}, []);
// 投稿一覧を取得する関数
const fetchPosts = async () => {
try {
setLoadingPosts(true);
// JSONPlaceholderの無料APIを使用
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const data = await response.json();
setPosts(data);
setError(null);
} catch (err) {
setError(err.message);
setPosts([]);
} finally {
setLoadingPosts(false);
}
};
// 入力フォームの変更ハンドラー
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewPost(prev => ({
...prev,
[name]: value
}));
};
// 新規投稿の送信ハンドラー
const handleSubmit = async (e) => {
e.preventDefault();
// バリデーション
if (!newPost.title.trim() || !newPost.body.trim()) {
setError('タイトルと本文は必須です');
return;
}
try {
setSubmitting(true);
setError(null);
// POSTリクエストの送信
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: newPost.title,
body: newPost.body,
userId: 1 // ダミーユーザーID
})
});
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
const createdPost = await response.json();
console.log('作成された投稿:', createdPost);
// 成功メッセージを表示
setSubmitSuccess(true);
// 投稿リストを更新(JSONPlaceholderは実際には更新されないため、手動で追加)
setPosts(prev => [createdPost, ...prev]);
// フォームをリセット
setNewPost({ title: '', body: '' });
// 3秒後に成功メッセージを非表示
setTimeout(() => {
setSubmitSuccess(false);
}, 3000);
} catch (err) {
setError(err.message);
} finally {
setSubmitting(false);
}
};
// スタイル定義
const styles = {
container: {
maxWidth: '800px',
margin: '0 auto',
padding: '20px',
fontFamily: 'Arial, sans-serif'
},
header: {
borderBottom: '2px solid #3498db',
paddingBottom: '10px',
marginBottom: '20px',
color: '#2c3e50'
},
form: {
marginBottom: '30px',
padding: '20px',
backgroundColor: '#f7f7f7',
borderRadius: '8px'
},
formGroup: {
marginBottom: '15px'
},
label: {
display: 'block',
marginBottom: '5px',
fontWeight: 'bold'
},
input: {
width: '100%',
padding: '8px',
fontSize: '16px',
borderRadius: '4px',
border: '1px solid #ddd'
},
textarea: {
width: '100%',
padding: '8px',
fontSize: '16px',
borderRadius: '4px',
border: '1px solid #ddd',
minHeight: '100px'
},
submitButton: {
backgroundColor: '#3498db',
color: 'white',
border: 'none',
padding: '10px 15px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px'
},
disabledButton: {
backgroundColor: '#95a5a6',
cursor: 'not-allowed'
},
errorMessage: {
backgroundColor: '#f8d7da',
color: '#721c24',
padding: '10px',
borderRadius: '4px',
marginBottom: '20px'
},
successMessage: {
backgroundColor: '#d4edda',
color: '#155724',
padding: '10px',
borderRadius: '4px',
marginBottom: '20px'
},
loadingIndicator: {
textAlign: 'center',
padding: '20px'
},
postList: {
listStyle: 'none',
padding: 0
},
postItem: {
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
marginBottom: '15px'
},
postTitle: {
margin: '0 0 10px 0',
color: '#3498db'
},
postBody: {
margin: 0,
color: '#555'
}
};
return (
<div style={styles.container}>
<h1 style={styles.header}>APIと連携するReactアプリ</h1>
{/* エラーメッセージ */}
{error && (
<div style={styles.errorMessage}>
{error}
</div>
)}
{/* 成功メッセージ */}
{submitSuccess && (
<div style={styles.successMessage}>
投稿が正常に作成されました!
</div>
)}
{/* 新規投稿フォーム */}
<div style={styles.form}>
<h2>新規投稿を作成</h2>
<form onSubmit={handleSubmit}>
<div style={styles.formGroup}>
<label style={styles.label} htmlFor="title">タイトル:</label>
<input
style={styles.input}
type="text"
id="title"
name="title"
value={newPost.title}
onChange={handleInputChange}
disabled={submitting}
/>
</div>
<div style={styles.formGroup}>
<label style={styles.label} htmlFor="body">内容:</label>
<textarea
style={styles.textarea}
id="body"
name="body"
value={newPost.body}
onChange={handleInputChange}
disabled={submitting}
/>
</div>
<button
style={{
...styles.submitButton,
...(submitting ? styles.disabledButton : {})
}}
type="submit"
disabled={submitting}
>
{submitting ? '送信中...' : '投稿する'}
</button>
</form>
</div>
{/* 投稿一覧 */}
<div>
<h2>投稿一覧</h2>
{loadingPosts ? (
<div style={styles.loadingIndicator}>読み込み中...</div>
) : posts.length > 0 ? (
<ul style={styles.postList}>
{posts.map(post => (
<li key={post.id} style={styles.postItem}>
<h3 style={styles.postTitle}>{post.title}</h3>
<p style={styles.postBody}>{post.body}</p>
</li>
))}
</ul>
) : (
<p>投稿がありません</p>
)}
</div>
</div>
);
}
export default ApiExample;
説明
Step 1APIとの連携の基本
Reactアプリケーションは、サーバーやAPIとデータをやり取りすることで機能性を拡張できます。一般的には、fetchやaxiosなどのライブラリを使ってHTTPリクエストを送信します。
基本的なAPIリクエストの流れ:
- コンポーネントがマウントまたは特定のイベントが発生したとき、APIリクエストを送信
- レスポンスを受け取り、データをstateに保存
- stateの変更によってコンポーネントが再レンダリングされ、取得したデータが表示される
Step 2fetchを使用したAPIリクエスト
JavaScriptの標準fetch APIを使用して、データを取得する基本的な例:
import React, { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // データ取得関数 const fetchUsers = async () => { try { // APIからデータを取得 const response = await fetch('https://jsonplaceholder.typicode.com/users'); // レスポンスが正常でない場合、エラーをスロー if (!response.ok) { throw new Error(`APIエラー: ${response.status}`); } // JSON形式のデータを解析 const data = await response.json(); // 取得したデータをstateに保存 setUsers(data); setLoading(false); } catch (error) { // エラーハンドリング setError(error.message); setLoading(false); } }; // データ取得を実行 fetchUsers(); }, []); // 空の依存配列でマウント時に1回だけ実行 // ローディング中の表示 if (loading) return <div>読み込み中...</div>; // エラーがある場合の表示 if (error) return <div>エラー: {error}</div>; // データを表示 return ( <div> <h1>ユーザー一覧</h1> <ul> {users.map(user => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> </div> ); }
Step 3POSTリクエストの送信
新しいデータを作成するためのPOSTリクエストの例:
import React, { useState } from 'react'; function CreatePost() { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (event) => { event.preventDefault(); setLoading(true); setError(null); try { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, body, userId: 1 // ダミーのユーザーID }) }); if (!response.ok) { throw new Error(`APIエラー: ${response.status}`); } const data = await response.json(); console.log('作成成功:', data); setSuccess(true); setTitle(''); setBody(''); } catch (error) { setError(error.message); } finally { setLoading(false); } }; return ( <div> <h1>新規投稿</h1> {success && <div className="success">投稿が作成されました!</div>} {error && <div className="error">エラー: {error}</div>} <form onSubmit={handleSubmit}> <div> <label htmlFor="title">タイトル:</label> <input type="text" id="title" value={title} onChange={(e) => setTitle(e.target.value)} required /> </div> <div> <label htmlFor="body">内容:</label> <textarea id="body" value={body} onChange={(e) => setBody(e.target.value)} required /> </div> <button type="submit" disabled={loading}> {loading ? '送信中...' : '投稿する'} </button> </form> </div> ); }
Step 4カスタムフックを使ったAPI連携
コードの再利用性を高めるために、API呼び出しをカスタムフックとして実装できます:
// useApi.js - カスタムフック import { useState, useEffect } from 'react'; export function useGet(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(`APIエラー: ${response.status}`); } const result = await response.json(); setData(result); } catch (error) { setError(error.message); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; } export function usePost() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const postData = async (url, payload) => { try { setLoading(true); setError(null); setSuccess(false); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`APIエラー: ${response.status}`); } const result = await response.json(); setData(result); setSuccess(true); return result; } catch (error) { setError(error.message); throw error; } finally { setLoading(false); } }; return { postData, data, loading, error, success }; }
// カスタムフックを使用したコンポーネント import React, { useState } from 'react'; import { useGet, usePost } from './useApi'; function UserDashboard() { const { data: users, loading: usersLoading, error: usersError } = useGet('https://jsonplaceholder.typicode.com/users'); const { postData, loading: postLoading, error: postError, success } = usePost(); const [newUser, setNewUser] = useState({ name: '', email: '' }); const handleSubmit = async (e) => { e.preventDefault(); try { await postData('https://jsonplaceholder.typicode.com/users', newUser); setNewUser({ name: '', email: '' }); } catch (error) { console.error('ユーザー作成エラー:', error); } }; return ( <div> <h1>ユーザーダッシュボード</h1> {/* ユーザー一覧 */} <h2>ユーザー一覧</h2> {usersLoading && <p>読み込み中...</p>} {usersError && <p>エラー: {usersError}</p>} {users && ( <ul> {users.map(user => ( <li key={user.id}>{user.name} ({user.email})</li> ))} </ul> )} {/* 新規ユーザー作成フォーム */} <h2>新規ユーザー作成</h2> {success && <p>ユーザーが作成されました!</p>} {postError && <p>エラー: {postError}</p>} <form onSubmit={handleSubmit}> <div> <label>名前:</label> <input type="text" value={newUser.name} onChange={(e) => setNewUser({...newUser, name: e.target.value})} required /> </div> <div> <label>メール:</label> <input type="email" value={newUser.email} onChange={(e) => setNewUser({...newUser, email: e.target.value})} required /> </div> <button type="submit" disabled={postLoading}> {postLoading ? '送信中...' : '作成'} </button> </form> </div> ); }
API連携のベストプラクティス:
- 適切なローディング状態とエラー処理を実装する
- エラーメッセージはユーザーに分かりやすく表示する
- 再利用可能なカスタムフックにAPI呼び出しをまとめる
- 複雑なAPIロジックはサービスレイヤーに分離する
- 大規模アプリケーションでは、React Query, SWR, Apolloなどの状態管理ライブラリの使用を検討する
- APIレスポンスをキャッシュし、同じリクエストの重複を避ける