ReactでAPIを叩く方法|fetchとaxiosの基本的な使い方

ReactでAPIリクエストを送る方法をfetchとaxiosで詳しく解説。基本的な使い方からエラーハンドリング、実践的なパターンまで初心者向けに紹介

Learning Next 運営
55 分で読めます

ReactでAPIを叩く方法、どうやって実装すればいいか迷っていませんか?

「fetchとaxiosどっちを使えばいいの?」 「非同期処理がよく分からない」 「エラーハンドリングって何?」

こんな疑問をお持ちの方も多いと思います。

この記事では、ReactでAPIリクエストを送る方法を初心者の方でも理解できるよう、具体的なコード例とともに詳しく解説します。 fetchとaxiosの両方を使った実装パターンから、エラーハンドリングまでカバーしています。

読み終わる頃には、自信を持ってAPIを扱えるようになっているはずです。

API通信の基本概念

まずは、ReactでAPI通信を行う際の基本的な概念から理解していきましょう。

HTTPメソッドの種類

APIとやり取りするときに使う主要なHTTPメソッドをご紹介します。

// HTTPメソッドとその用途
const httpMethods = {
  GET: {
    用途: 'データの取得',
    例: 'ユーザー一覧の取得、商品詳細の取得'
  },
  POST: {
    用途: '新しいデータの作成',
    例: 'ユーザー登録、新しい投稿の作成'
  },
  PUT: {
    用途: 'データの完全な更新',
    例: 'ユーザー情報の全体更新'
  },
  PATCH: {
    用途: 'データの部分的な更新',
    例: 'パスワードのみの変更'
  },
  DELETE: {
    用途: 'データの削除',
    例: 'ユーザーの削除、投稿の削除'
  }
};

このように、それぞれのHTTPメソッドには適切な用途があります。 GETはデータを取得するとき、POSTは新しいデータを作るときに使います。

Reactでの非同期処理

APIリクエストは時間がかかる処理なので、非同期で実行する必要があります。

// useEffect を使った基本的なパターン
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // コンポーネントマウント時にAPIを呼び出し
    fetchUsers();
  }, []); // 空の依存配列で初回のみ実行
  
  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/users');
      const data = await response.json();
      setUsers(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

このコードでは、useEffectでコンポーネントがマウントされたときにAPIを呼び出しています。 useStateでローディング状態とエラー状態を管理しているのがポイントです。

try-catch文を使ってエラーハンドリングも行っています。 これにより、API呼び出しが失敗したときにも適切に対処できます。

fetch APIの使い方

次に、ブラウザ標準のfetch APIを使ったHTTPリクエストの方法を見ていきましょう。

基本的なGETリクエスト

まずは、データを取得するGETリクエストから始めます。

// 基本的なGETリクエスト
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // fetchでGETリクエスト
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        
        // レスポンスのステータスチェック
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        // JSONデータの取得
        const userData = await response.json();
        setUser(userData);
        
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]);
  
  if (loading) return <div>ユーザー情報を読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>メール: {user.email}</p>
      <p>会社: {user.company.name}</p>
    </div>
  );
}

このコードの流れを詳しく見ていきましょう。

最初に、状態管理のためのstateを3つ定義しています。

const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

userは取得したユーザー情報を保存します。 loadingは現在データを読み込み中かどうかを示します。 errorはエラーが発生したときにエラーメッセージを保存します。

次に、実際のAPI呼び出し部分です。

const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

fetch関数でAPIにGETリクエストを送信しています。 awaitをつけることで、レスポンスが返ってくるまで待機します。

重要なのは、レスポンスのステータスチェックです。

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

fetchは、404エラーなどでも例外を投げません。 そのため、response.okでステータスコードをチェックする必要があります。

最後に、JSONデータを取得します。

const userData = await response.json();
setUser(userData);

response.json()でJSONデータを取得して、stateに保存します。

POSTリクエスト(データの送信)

今度は、データを送信するPOSTリクエストを見てみましょう。

// フォームデータの送信
function CreateUser() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  });
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');
  
  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      setLoading(true);
      setMessage('');
      
      // POSTリクエストでユーザー作成
      const response = await fetch('https://jsonplaceholder.typicode.com/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setMessage(`ユーザーが作成されました。ID: ${result.id}`);
      
      // フォームをリセット
      setFormData({ name: '', email: '', phone: '' });
      
    } catch (error) {
      setMessage(`エラー: ${error.message}`);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          名前:
          <input 
            type="text"
            name="name"
            value={formData.name}
            onChange={handleInputChange}
            required
          />
        </label>
      </div>
      
      <div>
        <label>
          メール:
          <input 
            type="email"
            name="email"
            value={formData.email}
            onChange={handleInputChange}
            required
          />
        </label>
      </div>
      
      <div>
        <label>
          電話番号:
          <input 
            type="tel"
            name="phone"
            value={formData.phone}
            onChange={handleInputChange}
          />
        </label>
      </div>
      
      <button type="submit" disabled={loading}>
        {loading ? '作成中...' : 'ユーザー作成'}
      </button>
      
      {message && <p>{message}</p>}
    </form>
  );
}

POSTリクエストはGETリクエストよりも少し複雑です。 一つずつ見ていきましょう。

まず、フォームデータの管理です。

const [formData, setFormData] = useState({
  name: '',
  email: '',
  phone: ''
});

フォームの入力値をオブジェクトで管理しています。

入力値の変更処理は以下のようになります。

const handleInputChange = (e) => {
  const { name, value } = e.target;
  setFormData(prev => ({
    ...prev,
    [name]: value
  }));
};

スプレッド演算子を使って、変更された項目だけを更新しています。

POSTリクエストの核心部分はこちらです。

const response = await fetch('https://jsonplaceholder.typicode.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(formData)
});

GETリクエストと違って、オプションオブジェクトを指定します。 method: 'POST'でPOSTリクエストを指定。 headersでコンテンツタイプをJSONに設定。 bodyで送信するデータを文字列に変換して設定しています。

PUT/PATCHリクエスト(データの更新)

データを更新するPUTとPATCHリクエストも見てみましょう。

// ユーザー情報の更新
function EditUser({ userId }) {
  const [user, setUser] = useState(null);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  });
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');
  
  // 初期データの取得
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
        setFormData({
          name: userData.name,
          email: userData.email,
          phone: userData.phone
        });
      } catch (error) {
        console.error('ユーザー取得エラー:', error);
      }
    };
    
    fetchUser();
  }, [userId]);
  
  const handleUpdate = async (e) => {
    e.preventDefault();
    
    try {
      setLoading(true);
      setMessage('');
      
      // PUTリクエストで全体更新
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData)
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setMessage('ユーザー情報が更新されました');
      
    } catch (error) {
      setMessage(`エラー: ${error.message}`);
    } finally {
      setLoading(false);
    }
  };
  
  const handlePartialUpdate = async (field, value) => {
    try {
      // PATCHリクエストで部分更新
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ [field]: value })
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      setMessage(`${field}が更新されました`);
      
    } catch (error) {
      setMessage(`エラー: ${error.message}`);
    }
  };
  
  if (!user) return <div>読み込み中...</div>;
  
  return (
    <div>
      <h2>ユーザー編集</h2>
      
      <form onSubmit={handleUpdate}>
        <div>
          <label>
            名前:
            <input 
              type="text"
              value={formData.name}
              onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
              onBlur={(e) => handlePartialUpdate('name', e.target.value)}
            />
          </label>
        </div>
        
        <div>
          <label>
            メール:
            <input 
              type="email"
              value={formData.email}
              onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
            />
          </label>
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? '更新中...' : '全体更新'}
        </button>
      </form>
      
      {message && <p>{message}</p>}
    </div>
  );
}

PUTとPATCHの違いについて説明します。

PUTリクエストは、データを完全に置き換えるときに使います。

// PUTリクエストで全体更新
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(formData)
});

全てのフィールドを含んだデータを送信します。

PATCHリクエストは、データの一部だけを更新するときに使います。

// PATCHリクエストで部分更新
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ [field]: value })
});

変更したいフィールドだけを送信します。

DELETEリクエスト

最後に、データを削除するDELETEリクエストを見てみましょう。

// データの削除
function UserManagement() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    fetchUsers();
  }, []);
  
  const fetchUsers = async () => {
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await response.json();
      setUsers(data);
    } catch (error) {
      console.error('ユーザー取得エラー:', error);
    }
  };
  
  const deleteUser = async (userId) => {
    if (!window.confirm('本当に削除しますか?')) {
      return;
    }
    
    try {
      setLoading(true);
      
      // DELETEリクエスト
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      // 成功した場合、UIからも削除
      setUsers(prev => prev.filter(user => user.id !== userId));
      alert('ユーザーが削除されました');
      
    } catch (error) {
      alert(`削除エラー: ${error.message}`);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      <h2>ユーザー管理</h2>
      
      {users.map(user => (
        <div key={user.id} style={{ border: '1px solid #ccc', padding: '10px', margin: '5px' }}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <button 
            onClick={() => deleteUser(user.id)}
            disabled={loading}
            style={{ backgroundColor: 'red', color: 'white' }}
          >
            削除
          </button>
        </div>
      ))}
    </div>
  );
}

DELETEリクエストは比較的シンプルです。

const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
  method: 'DELETE'
});

メソッドにDELETEを指定するだけです。 bodyは通常不要です。

重要なのは、削除前の確認処理です。

if (!window.confirm('本当に削除しますか?')) {
  return;
}

誤って削除されないよう、確認ダイアログを表示しています。

削除が成功したら、UIからも該当のデータを削除します。

setUsers(prev => prev.filter(user => user.id !== userId));

これにより、画面上からも該当のユーザーが消えます。

axiosの使い方

次に、axiosライブラリを使ったAPI通信の方法を見ていきましょう。 axiosは、fetchよりも高機能で使いやすいのが特徴です。

axiosのインストールと基本設定

まず、axiosをインストールします。

# axiosのインストール
npm install axios

そして、基本的な設定を行います。

// axiosの基本的なインポートと設定
import axios from 'axios';

// ベースURLの設定(オプション)
const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000, // 10秒でタイムアウト
  headers: {
    'Content-Type': 'application/json'
  }
});

// リクエストインターセプター(共通処理)
api.interceptors.request.use(
  (config) => {
    // リクエスト前の共通処理(例:認証トークンの追加)
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// レスポンスインターセプター(共通処理)
api.interceptors.response.use(
  (response) => {
    // レスポンス成功時の共通処理
    return response;
  },
  (error) => {
    // レスポンスエラー時の共通処理
    if (error.response?.status === 401) {
      // 認証エラーの場合はログアウト処理など
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

この設定コードの各部分を詳しく見ていきましょう。

まず、axiosインスタンスの作成です。

const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

axios.create()で専用のaxiosインスタンスを作成します。 baseURLを設定することで、毎回フルURLを書く必要がなくなります。 timeoutで10秒でタイムアウトするよう設定しています。

次に、リクエストインターセプターです。

api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

これは、全てのリクエストに対して共通の処理を行います。 認証トークンを自動的に追加する処理を入れています。

レスポンスインターセプターも同様に、全てのレスポンスに対して共通の処理を行います。

api.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

401エラー(認証エラー)が発生したら、自動的にログアウト処理を行います。

axiosでのGETリクエスト

axiosを使ったGETリクエストはfetchよりもシンプルです。

// axiosを使ったGETリクエスト
function UserListWithAxios() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // axiosでGETリクエスト(シンプル)
        const response = await axios.get('https://jsonplaceholder.typicode.com/users');
        
        // axiosは自動的にJSONをパースしてくれる
        setUsers(response.data);
        
      } catch (error) {
        // axiosのエラーハンドリング
        if (error.response) {
          // サーバーからのエラーレスポンス
          setError(`エラー ${error.response.status}: ${error.response.statusText}`);
        } else if (error.request) {
          // リクエストが送信されたがレスポンスがない
          setError('ネットワークエラー: サーバーに接続できません');
        } else {
          // その他のエラー
          setError(`エラー: ${error.message}`);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchUsers();
  }, []);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div style={{ color: 'red' }}>{error}</div>;
  
  return (
    <div>
      <h2>ユーザー一覧(axios版)</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

axiosとfetchの大きな違いを見てみましょう。

axiosの場合

const response = await axios.get('https://jsonplaceholder.typicode.com/users');
setUsers(response.data);

fetchの場合

const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUsers(data);

axiosの方が明らかにシンプルですね。 axiosは自動的にJSONをパースしてくれます。 また、ステータスコードのチェックも自動的に行われます。

axiosのエラーハンドリングも充実しています。

if (error.response) {
  // サーバーからのエラーレスポンス
  setError(`エラー ${error.response.status}: ${error.response.statusText}`);
} else if (error.request) {
  // リクエストが送信されたがレスポンスがない
  setError('ネットワークエラー: サーバーに接続できません');
} else {
  // その他のエラー
  setError(`エラー: ${error.message}`);
}

エラーの種類によって適切な処理を行えます。

axiosでのPOSTリクエスト

axiosでのPOSTリクエストも見てみましょう。

// axiosを使ったPOSTリクエスト
function CreateUserWithAxios() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  });
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      setLoading(true);
      setMessage('');
      
      // axiosでPOSTリクエスト
      const response = await axios.post('https://jsonplaceholder.typicode.com/users', formData);
      
      setMessage(`ユーザーが作成されました。ID: ${response.data.id}`);
      setFormData({ name: '', email: '', phone: '' });
      
    } catch (error) {
      if (error.response) {
        setMessage(`エラー ${error.response.status}: ${error.response.data.message || 'サーバーエラー'}`);
      } else {
        setMessage(`エラー: ${error.message}`);
      }
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input 
          type="text"
          placeholder="名前"
          value={formData.name}
          onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
          required
        />
      </div>
      
      <div>
        <input 
          type="email"
          placeholder="メールアドレス"
          value={formData.email}
          onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
          required
        />
      </div>
      
      <button type="submit" disabled={loading}>
        {loading ? '作成中...' : 'ユーザー作成'}
      </button>
      
      {message && <p>{message}</p>}
    </form>
  );
}

axiosでのPOSTリクエストは非常にシンプルです。

const response = await axios.post('https://jsonplaceholder.typicode.com/users', formData);

第1引数にURL、第2引数に送信するデータを指定するだけです。 JSONの変換やヘッダーの設定は自動的に行われます。

axiosでの同時リクエスト

axiosでは、複数のAPIを同時に呼び出すことも簡単です。

// 複数のAPIを同時に呼び出し
function DashboardWithAxios() {
  const [data, setData] = useState({
    users: [],
    posts: [],
    comments: []
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchDashboardData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // 複数のAPIを同時に呼び出し
        const [usersResponse, postsResponse, commentsResponse] = await Promise.all([
          axios.get('https://jsonplaceholder.typicode.com/users'),
          axios.get('https://jsonplaceholder.typicode.com/posts'),
          axios.get('https://jsonplaceholder.typicode.com/comments')
        ]);
        
        setData({
          users: usersResponse.data,
          posts: postsResponse.data,
          comments: commentsResponse.data
        });
        
      } catch (error) {
        setError('ダッシュボードデータの取得に失敗しました');
      } finally {
        setLoading(false);
      }
    };
    
    fetchDashboardData();
  }, []);
  
  if (loading) return <div>ダッシュボードを読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <div>
      <h2>ダッシュボード</h2>
      
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '20px' }}>
        <div>
          <h3>ユーザー数</h3>
          <p>{data.users.length}人</p>
        </div>
        
        <div>
          <h3>投稿数</h3>
          <p>{data.posts.length}件</p>
        </div>
        
        <div>
          <h3>コメント数</h3>
          <p>{data.comments.length}件</p>
        </div>
      </div>
    </div>
  );
}

同時リクエストの核心部分はこちらです。

const [usersResponse, postsResponse, commentsResponse] = await Promise.all([
  axios.get('https://jsonplaceholder.typicode.com/users'),
  axios.get('https://jsonplaceholder.typicode.com/posts'),
  axios.get('https://jsonplaceholder.typicode.com/comments')
]);

Promise.allを使って、3つのAPIリクエストを同時に実行しています。 順番に実行するよりも、大幅に時間を短縮できます。

3つのAPIが全て完了してから、次の処理に進みます。

setData({
  users: usersResponse.data,
  posts: postsResponse.data,
  comments: commentsResponse.data
});

取得したデータを一気にstateに設定します。

fetchとaxiosの比較

ここで、fetchとaxiosの特徴を比較してみましょう。

機能比較

// fetch vs axios の機能比較
const comparison = {
  fetch: {
    メリット: [
      "ブラウザ標準API(追加のライブラリ不要)",
      "軽量",
      "Promise ベース",
      "ストリーミング対応"
    ],
    デメリット: [
      "JSON の自動パースなし",
      "レスポンスステータスチェックが必要",
      "リクエスト/レスポンスインターセプターなし",
      "古いブラウザでサポート不足"
    ]
  },
  
  axios: {
    メリット: [
      "JSON の自動パース",
      "リクエスト/レスポンスインターセプター",
      "リクエストキャンセル機能",
      "豊富な設定オプション",
      "古いブラウザサポート"
    ],
    デメリット: [
      "外部ライブラリ(バンドルサイズ増加)",
      "学習コストがわずかに高い"
    ]
  }
};

fetchを選ぶべき場合

  • 軽量さを重視したい
  • 外部ライブラリを増やしたくない
  • シンプルなAPIリクエストのみ

axiosを選ぶべき場合

  • 開発効率を重視したい
  • 複雑なAPI通信を行う
  • 古いブラウザをサポートしたい

実装の違い

同じ機能をfetchとaxiosで実装した場合の違いを見てみましょう。

// 同じ機能をfetchとaxiosで実装

// fetch版
const fetchExample = async () => {
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
    
  } catch (error) {
    console.error('Fetch error:', error);
    throw error;
  }
};

// axios版
const axiosExample = async () => {
  try {
    const response = await axios.post('/api/users', userData, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    return response.data;
    
  } catch (error) {
    console.error('Axios error:', error);
    throw error;
  }
};

axiosの方が明らかにコード量が少ないですね。

fetchの場合

  • ヘッダーの設定が必要
  • JSONの変換が必要
  • ステータスチェックが必要

axiosの場合

  • これらが全て自動的に処理される
  • よりシンプルで読みやすい

実践的なパターン

実際の開発でよく使われるAPIリクエストのパターンをご紹介します。

カスタムフックでのAPI管理

APIリクエストの処理を再利用可能にするカスタムフックを作ってみましょう。

// カスタムフック:useApi
function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await axios.get(url, options);
      setData(response.data);
      
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  }, [url]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  const refetch = () => {
    fetchData();
  };
  
  return { data, loading, error, refetch };
}

// カスタムフックの使用例
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={refetch}>再取得</button>
    </div>
  );
}

このカスタムフックの便利な点を説明します。

まず、APIリクエストの共通処理をまとめています。

const fetchData = useCallback(async () => {
  try {
    setLoading(true);
    setError(null);
    
    const response = await axios.get(url, options);
    setData(response.data);
    
  } catch (error) {
    setError(error);
  } finally {
    setLoading(false);
  }
}, [url]);

ローディング状態の管理、エラーハンドリング、データの設定が全て含まれています。

使う側はとてもシンプルになります。

const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`);

URLを指定するだけで、データ取得の処理が完了します。 refetch関数で再取得も可能です。

フォーム送信のカスタムフック

フォーム送信専用のカスタムフックも作ってみましょう。

// カスタムフック:useFormSubmit
function useFormSubmit(url, method = 'POST') {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  
  const submit = async (data) => {
    try {
      setLoading(true);
      setError(null);
      setSuccess(false);
      
      const response = await axios({
        method,
        url,
        data
      });
      
      setSuccess(true);
      return response.data;
      
    } catch (error) {
      setError(error.response?.data?.message || error.message);
      throw error;
    } finally {
      setLoading(false);
    }
  };
  
  return { submit, loading, error, success };
}

// フォーム送信の使用例
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  const { submit, loading, error, success } = useFormSubmit('/api/contact');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      await submit(formData);
      setFormData({ name: '', email: '', message: '' }); // フォームリセット
    } catch (error) {
      // エラーは useFormSubmit で管理
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        placeholder="お名前"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        required
      />
      
      <input 
        type="email"
        placeholder="メールアドレス"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        required
      />
      
      <textarea 
        placeholder="メッセージ"
        value={formData.message}
        onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
        required
      />
      
      <button type="submit" disabled={loading}>
        {loading ? '送信中...' : '送信'}
      </button>
      
      {error && <p style={{ color: 'red' }}>エラー: {error}</p>}
      {success && <p style={{ color: 'green' }}>送信が完了しました</p>}
    </form>
  );
}

このカスタムフックの特徴は以下の通りです。

const { submit, loading, error, success } = useFormSubmit('/api/contact');
  • submit関数でフォームデータを送信
  • loadingで送信中の状態を管理
  • errorでエラーメッセージを管理
  • successで送信成功の状態を管理

フォームコンポーネントでは、状態管理の詳細を気にする必要がありません。

リアルタイム検索

最後に、リアルタイム検索の実装例を見てみましょう。

// リアルタイム検索の実装
function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // デバウンス処理
  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }
    
    const timeoutId = setTimeout(async () => {
      try {
        setLoading(true);
        
        const response = await axios.get('/api/users/search', {
          params: { q: query }
        });
        
        setResults(response.data);
        
      } catch (error) {
        console.error('検索エラー:', error);
      } finally {
        setLoading(false);
      }
    }, 300); // 300ms後に検索実行
    
    return () => clearTimeout(timeoutId);
  }, [query]);
  
  return (
    <div>
      <input 
        type="text"
        placeholder="ユーザーを検索..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      
      {loading && <div>検索中...</div>}
      
      <ul>
        {results.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

このリアルタイム検索の核心はデバウンス処理です。

const timeoutId = setTimeout(async () => {
  // 検索処理
}, 300);

return () => clearTimeout(timeoutId);

入力があるたびに即座に検索するのではなく、300ms待ってから検索を実行します。 新しい入力があると、前のタイマーをキャンセルして新しいタイマーを設定します。

これにより、無駄なAPIリクエストを減らすことができます。

クエリパラメータの送信も簡単です。

const response = await axios.get('/api/users/search', {
  params: { q: query }
});

paramsオプションを使うと、自動的にクエリパラメータに変換されます。 この例では、/api/users/search?q=入力値のようなURLになります。

まとめ

ReactでAPIを叩く方法について、fetchとaxiosの使い方を詳しく解説しました。

fetchの特徴

  • ブラウザ標準API
  • 軽量
  • JSONの手動パースが必要
  • ステータスチェックが必要

axiosの特徴

  • 高機能なライブラリ
  • 自動的なJSONパース
  • 豊富なオプション
  • インターセプター機能

どちらを選ぶかは、プロジェクトの要件によります。 軽量さを重視するならfetch、開発効率を重視するならaxiosがおすすめです。

カスタムフックを活用することで、APIリクエストの処理を再利用可能にできます。 エラーハンドリングやローディング状態の管理も忘れずに実装しましょう。

まずは簡単なGETリクエストから始めて、徐々に複雑な機能を実装していくのがおすすめです。 実際にAPIを叩いてみることで、React開発のスキルが向上します。

ぜひこの記事を参考に、ReactでのAPI通信にチャレンジしてみてください。

関連記事