JavaScriptでは、サーバーとの通信やタイマー待機など、時間がかかる処理を行う場合に非同期処理が使われます。非同期処理では「リクエストを送信しても、完了を待たずに他の処理を続行できる」ため、UIがフリーズせずスムーズな動作が可能になります。この背景として、JavaScriptはシングルスレッドで動作するため、一度に1つの処理しかできません。時間のかかる同期処理をそのまま行うと、他の操作(クリックやスクロールなど)がブロックされてしまいます。そのため、非同期通信(fetchやAJAXなど)が欠かせないのです。
従来はコールバック関数を多重にネストして非同期処理を記述していましたが、可読性や保守性が低い問題がありました。そこでPromiseが導入され、さらにその糖衣構文として登場したのがasync/awaitです。async/awaitでは、Promiseをより直感的に扱えるようになり、非同期処理をまるで同期的な手続きのように書けるため、可読性が大幅に向上します。例えば次のように書くと、API呼び出しの結果を順番に処理できます。
async function fetchData() {
try {
const res = await fetch('https://api.example.com/data');
const json = await res.json();
console.log(json);
} catch (error) {
console.error(error);
}
}
fetchData();
上記のコードでは、async functionを宣言するだけで関数は自動的にPromiseを返し、awaitでPromiseの完了を待ってから次の処理(res.json()の呼び出し)に進みます。async/awaitを使うと、.then()チェーンを書かなくても、まるで同期処理のように記述できる点が大きな特徴です。また、try...catchを使えばエラー処理も簡単に一元管理できます。
Reactでの非同期処理の使用例
Reactアプリケーションでも、外部APIからデータを取得したりサーバーにデータを送信したりする場面で非同期処理を多用します。代表的な例としては、次のようなケースがあります。
- 初回レンダリング時のデータ取得:ユーザー情報や商品リストなど、コンポーネント表示に必要なデータをサーバーから取得する。
- イベント発火時のAPI呼び出し:ボタンのクリックやフォーム送信に応じて、バックエンドのAPIを叩いて結果を取得・送信する。
- 定期更新やプッシュ通信:一定時間おきにデータを更新したり、WebSocketやSSEでサーバーからの通知を受け取ったりする。
これらの非同期処理を行うには、fetchやaxiosのようなAPI呼び出し手段を使用します。たとえば、fetchは内部的にPromiseを返す設計になっており、.then()やasync/awaitで扱えます。実際、Reactコンポーネント内でも以下のようにasync/awaitを使ってデータ取得することができます。
// 例:ボタンをクリックしてAPIからデータを取得するReactコンポーネント
import { useState } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const handleClick = async () => {
try {
const res = await fetch('https://api.example.com/items');
const json = await res.json();
setData(json);
} catch (e) {
console.error(e);
}
};
return (
<div>
<button onClick={handleClick}>データを取得</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
上記の例では、ボタンのonClickイベントで非同期関数handleClickを実行し、fetchで取得したデータを状態dataに保存して画面に表示しています。Reactではこのように、ユーザー操作に連動した非同期処理も簡単に扱えます。
useEffectでの非同期処理の扱い方
コンポーネントの初期表示(マウント)時にデータを自動的に取得したい場合は、useEffectフック内で非同期処理を行うのが一般的です。useEffectは「副作用」を扱うためのフックであり、画面のレンダリング後に実行したい処理(API呼び出しなど)を記述できます。ただしuseEffectには注意点があり、次のように直接async関数を渡すことはできません。
// ✖ NG:useEffectに直接asyncを書くとエラー
useEffect(async () => {
const res = await fetch('/api/data');
// ...
}, []);
useEffectに渡す関数は、クリーンアップのための関数を返す必要があるため、async(Promiseを返す)関数を渡すとReactが混乱し、エラーになるおそれがあります。このため、非同期処理を行いたい場合は、useEffect内で別途async関数を定義してから呼び出す方法を取ります。例として、次のように書けます。
useEffect(() => {
// IIFEで非同期処理を実行する例
(async () => {
try {
const res = await fetch('/api/data');
const json = await res.json();
console.log(json);
} catch (e) {
console.error(e);
}
})();
}, []);
// またはasync関数を定義してから呼び出す例
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/data');
const json = await res.json();
console.log(json);
} catch (e) {
console.error(e);
}
};
fetchData();
}, []);
上記のように、useEffect内で即時実行関数(IIFE)や関数宣言を用いれば、正しく非同期呼び出しが行えます。また、useEffectでは依存配列を指定しておくことが重要です。依存配列に必要な外部変数(propsやstateなど)を指定しないと、レンダリングのたびにuseEffectが再実行されて無限ループに陥ったり、逆に一度きりでその後に変数が更新されても処理が実行されなくなったりします。依存配列には、非同期処理内で参照するすべての変数を列挙しましょう。
エラーハンドリングと状態管理
非同期処理では、エラー処理を怠らないことが大切です。ネットワークリクエストは失敗する可能性があるため、何もせずPromiseを放置すると未処理の拒否(Unhandled Promise Rejection)になり、ブラウザコンソールにエラーが表示されたりアプリ全体が不安定になるおそれがあります。そのため、async関数内では必ずtry...catchでエラーを捕捉し、状況に応じてユーザーへ通知したりログに記録するようにします。また、処理の開始・終了に合わせて「読み込み中」の状態を管理することも重要です。例えば以下のようにloadingやerrorといったstateを用意しておけば、画面に適切なメッセージを出したり、ボタンを無効化したりできます。
import { useState } from 'react';
function FetchComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('https://api.example.com/items');
if (!res.ok) {
throw new Error(`HTTPエラー: ${res.status}`);
}
const json = await res.json();
setData(json);
} catch (e) {
setError(e.message || 'データ取得でエラーが発生しました');
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={fetchData} disabled={loading}>
{loading ? '読み込み中...' : 'データ取得'}
</button>
{error && <p style={{color:'red'}}>エラー: {error}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
上記の例では、fetchData関数内でtry...catch...finallyを使い、loading・error・dataの各状態を管理しています。エラーが起きればsetErrorで状態を記録し、finallyで必ずsetLoading(false)を呼ぶことで読み込み表示を終了します。このようにtry-catchでエラーを捕まえることと、読み込み・エラー用のstateを用意することは、Reactにおける非同期処理のベストプラクティスです。
よくある落とし穴
Reactで非同期処理を書く際によくある失敗例と対策をいくつか挙げます。
useEffectコールバックを直接asyncにしない
先述の通り、useEffect(async () => {...})のようにすると、Reactがコールバックの返り値をクリーンアップ関数と誤解し、エラーになります。必ずuseEffect内で別途非同期関数を定義して呼び出しましょう。- 依存配列の指定ミス
依存配列が空のままだと、propsやstateが変わってもuseEffectが再実行されず古いデータのままになります。一方で、依存配列をまったく書かないとレンダリングごとに毎回実行され、無限ループになる危険があります。useEffectで参照する外部変数はすべて依存配列に含めましょう。 - アンマウント後の状態更新
非同期処理中にコンポーネントがアンマウントされると、その後にsetStateしようとして「アンマウントされたコンポーネントに更新」を行うと警告が出ます。必要に応じてAbortControllerでフェッチを中止したり、マウント状態をフラグ管理して更新を抑制しましょう。(中級者向けの注意点ですが、よく起こります。)
これらの落とし穴に気をつければ、Reactの非同期処理は安定して実装できます。
応用例:ボタンでのデータ取得
最後に、実践的な例として「ボタンを押してデータを取得する処理」を示します。この例では、ボタンのクリック時に非同期関数handleClickを呼び出してAPIからデータを取得し、結果を画面に表示しています。エラー時や読み込み中の状態も状態管理することで、ユーザーにフィードバックできるようにしています。
import { useState } from 'react';
function ButtonFetchExample() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleClick = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('通信に失敗しました');
const json = await res.json();
setItems(json);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handleClick} disabled={loading}>
{loading ? '取得中...' : 'データ取得'}
</button>
{error && <p style={{color:'red'}}>エラー: {error}</p>}
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
上記コードでは、クリックイベントでhandleClickが実行され、非同期にJSONPlaceholderのAPIから投稿リストを取得します。処理中はボタンを無効化してテキストを「取得中…」に変更し、取得が終われば一覧を表示します。このようにasync/awaitとuseState、イベントハンドラを組み合わせれば、シンプルで分かりやすい非同期処理が実装できます。
以上、async/awaitの基本からReactでの具体的な使い方、useEffectとの連携、エラーハンドリング、注意点、そして応用例まで、段階的に解説しました。初心者の方でも少しずつ試して慣れていけば、非同期処理はすぐに理解できるようになります。


コメント