きっかけ
久しぶりにreactを使ってwebアプリケーションを開発したのだが、async/awaitの使い方を忘れていた。なので、もう一度学び直したい。
async/awaitとは
async/await
は、JavaScript の非同期処理をシンプルに書くための構文。非同期処理を同期的な見た目で記述できるため、コードの可読性が高まる。
-
async
を付けた関数は自動的に Promise を返す。 -
await
を使うと、Promise が解決されるまで処理を一時停止できる。ただしawait
はasync
関数の中でしか使えない。
本番のコードを使って解説
以下のコードは、現在地と天気情報を取得し、それをもとに詩を生成する React アプリの一部。非同期処理に async/await
を多用しているため、順を追って解説する。
1. fetchPoem
関数の定義
const fetchPoem = async () => { ... }
ここで async
を付けているため、関数内で await
を使えるようになる。
2. 現在地を取得
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
getCurrentPosition
はコールバック形式なので、Promise
にラップして await
で扱えるようにしている。
3. 天気APIの呼び出し
const weatherRes = await fetch(`https://api.openweathermap.org/...`);
const weatherData = await weatherRes.json();
fetch
は非同期関数。レスポンスが返るまで待ち、次に response.json()
を await
で待ってからデータを取得する。
4. 詩の生成APIの呼び出し
const res = await fetch("/api/GeneratePoem", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(environment),
});
const data = await res.json();
ここでも await
を使って、API呼び出しからレスポンスの取得、JSON 変換までを順番に処理している。
5. try-catch-finally でのエラーハンドリング
try {
...
} catch (error) {
console.error(error);
setPoem("詩の取得に失敗しました");
} finally {
setIsLoading(false);
}
非同期処理のどこかでエラーが起きても、catch
でエラーメッセージを出力し、最後にローディング状態を解除するようにしている。
6. 実行順番
-
コンポーネント
Home
の初回レンダリング -
useEffect
によりfetchPoem()
が一度だけ呼ばれる -
fetchPoem()
内で以下の非同期処理が順番に実行される:- ブラウザから位置情報を取得(
navigator.geolocation.getCurrentPosition
) - OpenWeather API で天気情報を取得(
fetch
) -
weatherRes.json()
でレスポンスをパース - 環境情報を整形
- 自作 API
/api/GeneratePoem
に POST リクエスト - 詩のデータを取得し、
setPoem()
で更新
- ブラウザから位置情報を取得(
-
finally 節で
setIsLoading(false)
を実行 -
poem
またはisLoading
の state 変更により再レンダリング
コード全文
'use client'; // このファイルはクライアントコンポーネントとして扱う
import { useEffect, useState } from "react";
import Header from "./components/layouts/Header/Header";
import Footer from "./components/layouts/Footer/Footer";
import CoccoCharacter from "./features/home/components/CoccoCharacter/CoccoCharacter";
import PoemBubble from "./features/home/components/PoemBubble/PoemBubble";
// Home コンポーネントの定義
export default function Home() {
// 詩の内容を保持する state
const [poem, setPoem] = useState("");
// 読み込み中かどうかを示す state
const [isLoading, setIsLoading] = useState(true);
// コンポーネントの初回マウント時に実行
useEffect(() => {
// 詩を取得する非同期関数
const fetchPoem = async () => {
try {
// 現在の位置情報を取得(Promise にラップして await を使用可能に)
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
// 緯度・経度の取得
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// OpenWeather API を使って現在地の天気情報を取得
const weatherRes = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${process.env.NEXT_PUBLIC_WEATHER_API_KEY}&units=metric&lang=ja`
);
// 天気情報を JSON に変換
const weatherData = await weatherRes.json();
// 詩の生成に必要な環境データを整形
const environment = {
location: weatherData.name, // 地名
temperature: weatherData.main.temp, // 気温
humidity: weatherData.main.humidity, // 湿度
weather: weatherData.weather[0].description, // 天気の説明(例:くもり、晴れなど)
time: new Date().toLocaleString("sv-SE", { timeZone: "Asia/Tokyo" }).replace(" ", "T") + ":00.000+09:00", // ISO8601形式に近い日時
};
// サーバーサイドのAPIに POST リクエストを送って詩を生成
const res = await fetch("/api/GeneratePoem", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(environment), // 環境情報を送信
});
// レスポンスを JSON として受け取り、詩のテキストを抽出
const data = await res.json();
setPoem(data.text || "詩の取得に失敗しました"); // テキストがなければエラーメッセージを表示
} catch (error) {
// 位置情報取得やAPI呼び出しでエラーが発生した場合
console.error(error);
setPoem("詩の取得に失敗しました");
} finally {
// 読み込み状態を終了
setIsLoading(false);
}
};
// 詩の取得関数を実行
fetchPoem();
}, []); // 空配列のため、初回マウント時のみ実行
// JSXの返却(UIレンダリング)
return (
<>
<Header /> {/* ヘッダーコンポーネント */}
<main className="min-h-screen bg-[#40494F] text-white flex flex-col">
<div className="flex-grow flex items-start justify-center pt-16 sm:pt-32">
<div className="flex flex-col items-center gap-4 sm:gap-6">
{/* 詩の吹き出しコンポーネント。読み込み中は「考え中...」と表示 */}
<PoemBubble poem={isLoading ? "考え中..." : poem} />
{/* キャラクターのアニメーションなどに使用。読み込み状態を渡す */}
<CoccoCharacter isLoading={isLoading} />
</div>
</div>
</main>
<Footer /> {/* フッターコンポーネント */}
</>
);
}