Reactの非同期処理が難しい|Promise基礎から理解する
React非同期処理の基礎をPromiseから丁寧に解説。API呼び出し、useEffect、async/await、エラーハンドリングまで実例付きで説明します。
「React で非同期処理を使いたいけど、難しそう」 「Promise って何?どうやって使うの?」
そんな疑問を持ったことはありませんか? React で API を呼び出したり、データを取得したりする場面は頻繁にあります。 でも、非同期処理の仕組みを理解していないと、なかなか思った通りに動きません。
この記事では、React の非同期処理を Promise の基礎から丁寧に解説します。 初心者の方でも理解できるように、実際のコード例とともに step by step で学んでいきましょう。
難しく感じるかもしれませんが、大丈夫です! 一つずつ順番に進めていけば、きっと理解できるはずです。
非同期処理って何?基本から理解しよう
まずは非同期処理の基本的な概念から理解していきましょう。 普通の処理(同期処理)と何が違うのか見てみます。
同期処理と非同期処理の違いを見てみよう
同期処理(Synchronous)はこんな感じです
// 同期処理の例console.log('1番目の処理');console.log('2番目の処理');console.log('3番目の処理');// 結果: 1番目 → 2番目 → 3番目 の順番で実行
このコードは、上から順番に実行されます。 1番目が終わってから2番目、2番目が終わってから3番目という感じです。
非同期処理(Asynchronous)はこんな感じです
// 非同期処理の例console.log('1番目の処理');setTimeout(() => { console.log('2番目の処理(遅延)');}, 1000);console.log('3番目の処理');// 結果: 1番目 → 3番目 → 2番目(1秒後)の順番で実行
非同期処理では、2番目の処理を待たずに3番目の処理が実行されます。 つまり、処理が並行して実行されるんです。
なぜ非同期処理が必要なの?
Web 開発では、時間のかかる処理がたくさんあります。 そういった処理を同期的に行うと、画面が固まってしまいます。
時間のかかる処理の例をいくつか見てみましょう
- API からのデータ取得
- データベースへの接続
- ファイルのアップロード/ダウンロード
- 画像の読み込み
// API呼び出しの例(時間がかかる処理)function fetchUserData() { // サーバーからデータを取得(1-2秒かかる) return fetch('/api/user') .then(response => response.json());}
この処理を同期的に行うと、データが取得できるまで画面が固まってしまいます。 それを防ぐために、非同期処理を使うんです。
非同期処理の身近な例
日常生活で例えると、こんな感じです。
同期処理の例:
- 洗濯をする
- 洗濯が終わるまで待つ
- 掃除をする
非同期処理の例:
- 洗濯機を回す
- 洗濯機が動いている間に掃除をする
- 洗濯が終わったら洗濯物を干す
非同期処理の方が効率的ですよね。 プログラムでも同じような考え方をします。
Promise って何?基本概念を理解しよう
Promise は、JavaScript の非同期処理を扱うための仕組みです。 「約束」という意味の通り、「後で結果を渡しますよ」という約束を表現します。
Promise の3つの状態
Promise には3つの状態があります。
// Promiseの状態const promise = new Promise((resolve, reject) => { // pending(保留中): 処理がまだ完了していない状態 setTimeout(() => { const success = true; if (success) { resolve('成功しました'); // fulfilled(成功) } else { reject('失敗しました'); // rejected(失敗) } }, 1000);});
- pending(保留中): 処理がまだ完了していない状態
- fulfilled(成功): 処理が成功した状態
- rejected(失敗): 処理が失敗した状態
基本的な Promise の使い方
Promise を作成して使ってみましょう。
// Promiseを使った基本的な処理function fetchData() { return new Promise((resolve, reject) => { // 模擬的なAPI呼び出し setTimeout(() => { const data = { id: 1, name: 'ユーザー1' }; resolve(data); // 成功時 }, 1000); });}
// Promiseの使用fetchData() .then(data => { console.log('データ取得成功:', data); }) .catch(error => { console.error('エラー:', error); });
このコードを詳しく見てみましょう。
fetchData
関数は Promise を返します。
1秒後にデータを返すという約束をしています。
.then()
メソッドで成功時の処理を書きます。
.catch()
メソッドで失敗時の処理を書きます。
Promise チェーンで複数の処理を繋げよう
複数の非同期処理を連続して実行したい場合は、Promise チェーンを使います。
// 複数の非同期処理を連鎖させるfunction fetchUserData() { return fetch('/api/user') .then(response => response.json()) .then(user => { console.log('ユーザー情報:', user); return fetch(`/api/user/${user.id}/posts`); }) .then(response => response.json()) .then(posts => { console.log('投稿一覧:', posts); return posts; }) .catch(error => { console.error('エラーが発生:', error); });}
このコードの流れを説明しますね。
まず、/api/user
からユーザー情報を取得します。
次に、取得したユーザー ID を使って投稿一覧を取得します。
最後に、投稿一覧を返します。
このように、.then()
を連続して使うことで、複数の処理を順番に実行できます。
React で Promise を活用してみよう
React で Promise を使用する具体的な方法を見ていきましょう。 実際のコンポーネントでどのように使うか学んでいきます。
useEffect で API 呼び出しをしてみよう
最も基本的な使い方は、useEffect
でのAPI呼び出しです。
import React, { useState, useEffect } from 'react';
function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // API呼び出し fetch('/api/user/123') .then(response => { if (!response.ok) { throw new Error('ユーザーの取得に失敗しました'); } return response.json(); }) .then(userData => { setUser(userData); setLoading(false); }) .catch(error => { setError(error.message); setLoading(false); }); }, []); if (loading) return <div>読み込み中...</div>; if (error) return <div>エラー: {error}</div>; return ( <div> <h1>{user.name}</h1> <p>メール: {user.email}</p> </div> );}
このコードを詳しく見てみましょう。
useState
で3つの状態を管理しています。
user
: 取得したユーザー情報loading
: データ取得中かどうかerror
: エラー情報
useEffect
内で API を呼び出しています。
成功時は setUser
でデータを設定し、失敗時は setError
でエラーを設定します。
複数の API 呼び出しを同時に実行しよう
複数の API を同時に呼び出したい場合は、Promise.all
を使います。
function UserDashboard() { const [data, setData] = useState({ user: null, posts: [], notifications: [] }); const [loading, setLoading] = useState(true); useEffect(() => { // 複数のAPIを並行して呼び出し Promise.all([ fetch('/api/user').then(res => res.json()), fetch('/api/posts').then(res => res.json()), fetch('/api/notifications').then(res => res.json()) ]) .then(([user, posts, notifications]) => { setData({ user, posts, notifications }); setLoading(false); }) .catch(error => { console.error('データ取得エラー:', error); setLoading(false); }); }, []); if (loading) return <div>読み込み中...</div>; return ( <div> <h1>ダッシュボード</h1> <UserInfo user={data.user} /> <PostList posts={data.posts} /> <NotificationList notifications={data.notifications} /> </div> );}
Promise.all
を使うことで、3つのAPIを同時に実行できます。
すべてのAPIが成功した場合のみ、.then()
が実行されます。
エラーハンドリングをしっかりしよう
API呼び出しでは、様々なエラーが発生する可能性があります。 適切なエラーハンドリングを行いましょう。
function DataFetcher() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const fetchData = () => { setLoading(true); setError(null); // 前回のエラーをクリア fetch('/api/data') .then(response => { // HTTPエラーの確認 if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } return response.json(); }) .then(result => { setData(result); }) .catch(error => { // エラーの種類に応じた処理 if (error.name === 'TypeError') { setError('ネットワークエラーが発生しました'); } else { setError(error.message); } }) .finally(() => { setLoading(false); }); }; return ( <div> <button onClick={fetchData} disabled={loading}> {loading ? '読み込み中...' : 'データを取得'} </button> {error && ( <div className="error"> エラー: {error} </div> )} {data && ( <div className="data"> <pre>{JSON.stringify(data, null, 2)}</pre> </div> )} </div> );}
このコードでは、HTTPエラーやネットワークエラーを適切に処理しています。
.finally()
を使って、処理が完了した時に必ず実行される処理を書いています。
async/await でもっと読みやすく書こう
async/await は、Promise をより読みやすく書くための構文です。 同期処理のような書き方で、非同期処理を書くことができます。
基本的な使い方を比較してみよう
まずは Promise と async/await の書き方を比較してみましょう。
// Promiseを使った書き方function fetchUserWithPromise() { return fetch('/api/user') .then(response => response.json()) .then(user => { console.log('ユーザー:', user); return user; }) .catch(error => { console.error('エラー:', error); });}
// async/awaitを使った書き方async function fetchUserWithAsync() { try { const response = await fetch('/api/user'); const user = await response.json(); console.log('ユーザー:', user); return user; } catch (error) { console.error('エラー:', error); }}
async/await を使った方が、読みやすいですよね。
await
を使うことで、非同期処理の完了を待つことができます。
React で async/await を使ってみよう
React コンポーネントでも async/await を使えます。
function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { // async関数を定義してすぐに実行 const fetchUsers = async () => { try { setLoading(true); const response = await fetch('/api/users'); if (!response.ok) { throw new Error('ユーザー一覧の取得に失敗しました'); } const usersData = await response.json(); setUsers(usersData); } catch (error) { console.error('エラー:', error); } finally { setLoading(false); } }; fetchUsers(); }, []); if (loading) return <div>読み込み中...</div>; return ( <div> <h1>ユーザー一覧</h1> {users.map(user => ( <div key={user.id}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} </div> );}
注意点として、useEffect
の中で直接 async
は使えません。
そのため、useEffect
内で async 関数を定義して、それを実行しています。
複数の非同期処理を順番に実行しよう
async/await を使うと、複数の非同期処理を順番に実行するのが簡単です。
async function fetchUserPosts(userId) { try { // まずユーザー情報を取得 const userResponse = await fetch(`/api/users/${userId}`); const user = await userResponse.json(); // 次に投稿一覧を取得 const postsResponse = await fetch(`/api/users/${userId}/posts`); const posts = await postsResponse.json(); // 最後にコメント一覧を取得 const commentsResponse = await fetch(`/api/users/${userId}/comments`); const comments = await commentsResponse.json(); return { user, posts, comments }; } catch (error) { console.error('データ取得エラー:', error); throw error; }}
このように、await
を使うことで処理の順序が明確になります。
Promise チェーンよりも読みやすく、理解しやすいコードになります。
エラーハンドリングをマスターしよう
非同期処理では、エラーハンドリングがとても重要です。 ネットワークエラーやサーバーエラーなど、様々なエラーが発生する可能性があります。
try-catch を使った基本的なエラーハンドリング
async/await では、try-catch を使ってエラーを処理します。
async function fetchDataWithErrorHandling() { try { const response = await fetch('/api/data'); // HTTPエラーの確認 if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { // エラーの種類に応じた処理 if (error.name === 'TypeError') { console.error('ネットワークエラー:', error.message); } else { console.error('その他のエラー:', error.message); } // エラーを再度投げる(必要に応じて) throw error; }}
エラーの種類を判定することで、適切な対応を取ることができます。
カスタムフックでエラーハンドリングを共通化しよう
エラーハンドリングを含む API 呼び出しを、カスタムフックにまとめることができます。
// エラーハンドリング用のカスタムフックfunction useApi(url) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const fetchData = async () => { try { setLoading(true); setError(null); const response = await fetch(url); if (!response.ok) { // カスタムエラーメッセージ const errorMessage = await response.text(); throw new Error(errorMessage || 'データの取得に失敗しました'); } const result = await response.json(); setData(result); } catch (error) { setError(error.message); } finally { setLoading(false); } }; return { data, error, loading, fetchData };}
// 使用例function UserProfile({ userId }) { const { data: user, error, loading, fetchData } = useApi(`/api/users/${userId}`); useEffect(() => { fetchData(); }, [userId]); if (loading) return <div>読み込み中...</div>; if (error) return <div>エラー: {error}</div>; if (!user) return <div>ユーザーが見つかりません</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );}
このカスタムフックを使うことで、コンポーネントでは簡単にAPIを呼び出すことができます。
エラーリトライ機能を実装しよう
ネットワークエラーなど、一時的なエラーの場合は、リトライ機能があると便利です。
async function fetchWithRetry(url, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } return await response.json(); } catch (error) { console.warn(`リトライ ${i + 1}/${maxRetries}:`, error.message); // 最後のリトライでもエラーの場合は、エラーを投げる if (i === maxRetries - 1) { throw error; } // 少し待ってからリトライ await new Promise(resolve => setTimeout(resolve, 1000)); } }}
このようにリトライ機能を実装することで、一時的なエラーからの回復が可能になります。
実践的な非同期処理パターンを学ぼう
実際の開発でよく使われる非同期処理のパターンを紹介します。 これらのパターンを覚えておくと、様々な場面で活用できます。
検索機能をデバウンスで実装しよう
検索機能では、入力のたびに API を呼び出すのは効率的ではありません。 デバウンス(少し待ってから実行)を使って最適化しましょう。
function UserSearch() { const [searchTerm, setSearchTerm] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); // デバウンス処理 useEffect(() => { if (!searchTerm) { setResults([]); return; } const timeoutId = setTimeout(async () => { setLoading(true); try { const response = await fetch(`/api/search?q=${encodeURIComponent(searchTerm)}`); const data = await response.json(); setResults(data); } catch (error) { console.error('検索エラー:', error); } finally { setLoading(false); } }, 300); // 300ms後に実行 return () => clearTimeout(timeoutId); // クリーンアップ }, [searchTerm]); return ( <div> <input type="text" placeholder="ユーザーを検索..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> {loading && <div>検索中...</div>} <div> {results.map(user => ( <div key={user.id}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} </div> </div> );}
このコードでは、入力が止まってから300ms後に検索を実行します。 これにより、無駄な API 呼び出しを減らすことができます。
フォームの非同期送信を実装しよう
フォームの送信も非同期処理で行うことが多いです。
function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '' }); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); try { setSubmitting(true); const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData) }); if (!response.ok) { throw new Error('送信に失敗しました'); } setSubmitted(true); setFormData({ name: '', email: '', message: '' }); // フォームをリセット } catch (error) { console.error('送信エラー:', error); alert('送信に失敗しました。もう一度お試しください。'); } finally { setSubmitting(false); } }; if (submitted) { return <div>お問い合わせありがとうございました!</div>; } return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="お名前" value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} required /> <input type="email" placeholder="メールアドレス" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} required /> <textarea placeholder="メッセージ" value={formData.message} onChange={(e) => setFormData({...formData, message: e.target.value})} required /> <button type="submit" disabled={submitting}> {submitting ? '送信中...' : '送信'} </button> </form> );}
このフォームでは、送信中は再度送信できないようにボタンを無効化しています。 また、成功時と失敗時の適切なフィードバックも提供しています。
リクエストのキャンセル機能を実装しよう
長時間かかるリクエストは、ユーザーがページを離れた時にキャンセルできるようにしましょう。
function CancellableRequest() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { const controller = new AbortController(); const fetchData = async () => { try { setLoading(true); const response = await fetch('/api/data', { signal: controller.signal // キャンセル可能にする }); const result = await response.json(); setData(result); } catch (error) { if (error.name === 'AbortError') { console.log('リクエストがキャンセルされました'); } else { console.error('エラー:', error); } } finally { setLoading(false); } }; fetchData(); // クリーンアップ時にリクエストをキャンセル return () => controller.abort(); }, []); return ( <div> {loading && <div>読み込み中...</div>} {data && <div>{JSON.stringify(data)}</div>} </div> );}
AbortController
を使うことで、リクエストをキャンセルできます。
これにより、不要なリクエストを停止できます。
よくある問題と解決方法
React で非同期処理を使う際によくある問題と、その解決方法を紹介します。 これらを知っておくことで、トラブルを避けることができます。
問題1: useEffect で直接 async を使おうとしてしまう
初心者の方がよくやってしまう間違いです。
// ❌ 間違い:useEffectで直接asyncを使用function BadExample() { const [data, setData] = useState(null); useEffect(async () => { const response = await fetch('/api/data'); const result = await response.json(); setData(result); }, []); // この書き方は推奨されない return <div>{data}</div>;}
// ✅ 正しい:useEffect内で async関数を定義function GoodExample() { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { const response = await fetch('/api/data'); const result = await response.json(); setData(result); }; fetchData(); }, []); return <div>{data}</div>;}
useEffect
の第一引数に直接 async
関数を渡すことはできません。
代わりに、useEffect
内で async
関数を定義して実行しましょう。
問題2: 競合状態(Race Condition)が発生する
複数のリクエストが同時に実行される際に起こる問題です。
// ❌ 競合状態の問題function RaceConditionExample() { const [userId, setUserId] = useState(1); const [user, setUser] = useState(null); useEffect(() => { const fetchUser = async () => { const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); setUser(userData); // 古いリクエストの結果が後から到着する可能性 }; fetchUser(); }, [userId]); return <div>{user?.name}</div>;}
// ✅ 解決方法:フラグを使用function RaceConditionSolution() { const [userId, setUserId] = useState(1); const [user, setUser] = useState(null); useEffect(() => { let isCancelled = false; const fetchUser = async () => { const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); if (!isCancelled) { setUser(userData); } }; fetchUser(); return () => { isCancelled = true; }; }, [userId]); return <div>{user?.name}</div>;}
isCancelled
フラグを使うことで、古いリクエストの結果を無視できます。
問題3: エラーハンドリングを忘れてしまう
非同期処理では、必ずエラーハンドリングを行いましょう。
// ❌ エラーハンドリングなしfunction NoErrorHandling() { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { const response = await fetch('/api/data'); const result = await response.json(); setData(result); }; fetchData(); // エラーが発生した場合の処理がない }, []); return <div>{data}</div>;}
// ✅ 適切なエラーハンドリングfunction WithErrorHandling() { const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error('データの取得に失敗しました'); } const result = await response.json(); setData(result); } catch (error) { setError(error.message); } }; fetchData(); }, []); if (error) return <div>エラー: {error}</div>; return <div>{data}</div>;}
エラーハンドリングを適切に行うことで、ユーザーに適切なフィードバックを提供できます。
まとめ:非同期処理をマスターしよう
React の非同期処理について、Promise の基礎から実践的な活用方法まで詳しく解説しました。 学んだ内容を整理してみましょう。
今回学んだ重要なポイント
基本概念の理解
- 非同期処理は時間のかかる処理を扱うために必要
- Promise は非同期処理の状態を管理する仕組み
- async/await は Promise をより読みやすく書く構文
これらの概念を理解することで、非同期処理の仕組みが分かります。
React での実践的な活用法
useEffect
内で API 呼び出しを行う- 適切なエラーハンドリングを実装する
- ローディング状態の管理を行う
React での非同期処理は、この基本パターンを覚えておけば大丈夫です。
実践的なテクニック
- デバウンス処理で検索機能を最適化
AbortController
でリクエストをキャンセル- カスタムフックで共通処理をまとめる
これらのテクニックを使うことで、より良いユーザーエクスペリエンスを提供できます。
よくある問題の回避方法
useEffect
で直接async
を使用しない- 競合状態を防ぐためのフラグを使用
- 適切なエラーハンドリングを実装
これらの問題を知っておくことで、トラブルを避けることができます。
学習を続けるためのステップ
1. 基本パターンをマスターしよう
- まず Promise の基本概念を理解する
- 簡単な API 呼び出しから始める
- エラーハンドリングを必ず追加する
2. 実践的なパターンを学ぼう
- 検索機能やフォーム送信を実装してみる
- リクエストのキャンセル機能を試してみる
- カスタムフックを作成してみる
3. 更なる学習に挑戦しよう
- リアルタイム通信(WebSocket)を学ぶ
- 状態管理ライブラリと組み合わせる
- テスト手法を学ぶ
最後に
非同期処理は最初は難しく感じるかもしれませんが、基本を理解すれば必ず使いこなせるようになります。 今回学んだ内容を基に、実際のプロジェクトで少しずつ実践してみてください。
大丈夫です! 一つずつステップアップしていけば、きっと素晴らしい React アプリケーションが作れるようになります。 ぜひ挑戦してみてくださいね。