React

ReactでREST API連携|データ取得と表示の実装

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リクエストの流れ:

  1. コンポーネントがマウントまたは特定のイベントが発生したとき、APIリクエストを送信
  2. レスポンスを受け取り、データをstateに保存
  3. 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レスポンスをキャッシュし、同じリクエストの重複を避ける