Reactの非同期処理が難しい|Promise基礎から理解する

React非同期処理の基礎をPromiseから丁寧に解説。API呼び出し、useEffect、async/await、エラーハンドリングまで実例付きで説明します。

Learning Next 運営
40 分で読めます

「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 アプリケーションが作れるようになります。 ぜひ挑戦してみてくださいね。

関連記事