JavaScriptとReact: 非同期処理、Promise、asyncとawaitを理解する
はじめに
現代のウェブ開発では、非同期処理は避けて通れないトピックです。特にReactやNext.jsを使用する際には、コンポーネントの非同期処理を適切に扱う必要があります。この記事では、よくある混乱ポイントである非同期処理、Promise、asyncとawaitについて、具体的な例を交えながら解説します。
非同期処理とは?
非同期処理とは、プログラムの実行を待たずに次の処理に進み、処理が完了したら結果を受け取る仕組みです。
一般的な非同期処理の例:
// APIからのデータ取得 async function fetchUserData() { const response = await fetch('https://api.example.com/users'); const data = await response.json(); return data; } // データベースの操作 async function getUserFromDB() { const user = await db.users.findOne({ id: 1 }); return user; }
同期処理と非同期処理の違い
// 同期処理の例 function synchronousProcess() { console.log('1. 開始'); // この処理が終わるまで次に進まない const result = heavyCalculation(); console.log('2. 終了'); } // 非同期処理の例 async function asynchronousProcess() { console.log('1. 開始'); // この処理は裏で実行され、完了を待たずに次の処理に進む fetch('https://api.example.com/data') .then(data => console.log('3. データ取得完了')); console.log('2. 次の処理'); }
Promiseとは?
Promiseは「約束」のようなものです。「後で結果を返すことを約束する」オブジェクトです。
// お母さんとの約束を表現する例 console.log('子供:お母さん、おやつちょうだい!'); // 「約束」を作る const promise = new Promise((resolve) => { console.log('お母さん:10分待ってね'); // 10分(ここでは3秒)後におやつをあげる setTimeout(() => { console.log('お母さん:はい、どうぞ!'); resolve('🍪 クッキー'); // 約束を果たす }, 3000); });
この例でPromiseは:
- 「後でおやつをあげる」という約束
- resolveは約束を果たすこと
- 3秒待つのは「約束が果たされるまでの時間」
asyncとawait
asyncとawaitは、Promiseをより読みやすく扱うための構文です。
// Promiseを使った書き方 function getData() { fetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data)); } // asyncとawaitを使った書き方 async function getData() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); }
Next.jsのServer Componentでの非同期処理
Next.jsのServer Componentでは、非同期データ取得がとても簡単になります:
// ユーザーデータを取得するページコンポーネント export default async function UserPage() { // APIからデータを取得(非同期処理) const userData = await fetch('https://api.example.com/user').then(res => res.json()); return ( <div> <h1>ユーザー情報</h1> <p>名前: {userData.name}</p> </div> ); }
この場合、awaitの部分で処理が一時停止し、fetchが完了するまでreturnしません。つまり、データ取得が完了するまでコンポーネントがレンダリングされないことを意味します。
コンポーネント内での非同期処理の扱い方
非同期処理を行うコンポーネントとそれを使う親コンポーネントの関係を見てみましょう:
// 親コンポーネント export default function App() { return ( <div> <h1>こんにちは</h1> {/* すぐに表示される */} <UserPage /> {/* この中で非同期処理が走る */} <Footer /> {/* UserPageを待たずに実行される */} </div> ); } // 非同期処理を行う子コンポーネント async function UserPage() { console.log('UserPageが実行開始'); const data = await fetch('...'); // ここで待つ console.log('データ取得完了'); return <div>{data.name}</div>; } // 別の子コンポーネント function Footer() { console.log('Footerが実行開始'); return <footer>フッター</footer>; }
この例では:
Suspenseを使った適切なローディング表示
より良いユーザー体験を提供するために、Suspenseを使ってローディング状態を表示できます:
export default function App() { return ( <div> <h1>こんにちは</h1> <Suspense fallback={<div>読み込み中...</div>}> <UserPage /> {/* このコンポーネントはデータ取得を待つ */} </Suspense> <Footer /> {/* これは待たない */} </div> ); }
asyncがない場合どうなるか?
コンポーネントにasyncをつけないと、Promiseが即座に返されて処理が進みます:
// asyncなし function UserPage() { console.log('UserPageが実行開始'); const dataPromise = fetch('https://api.example.com/data') // Promiseを返す .then(res => { console.log('データ取得完了'); return res.json(); }); console.log('すぐに実行される'); // エラー: Promiseの結果を直接使えない return <div>{dataPromise.name}</div>; }
このコードはエラーになります。なぜなら:
- fetchはPromiseを返す
- awaitがないのでPromiseの結果を待たない
- そのためdataPromise.nameは undefined になる
非同期処理の分かりやすい例
タイマーを使った例で非同期処理を理解してみましょう:
// 普通の関数(同期処理) function normalAdd(a, b) { console.log('計算開始'); const result = a + b; console.log('計算終了'); return result; } // 時間のかかる関数(非同期処理) async function slowAdd(a, b) { console.log('計算開始'); // 2秒待つ(時間のかかる処理の代わり) await new Promise(r => setTimeout(r, 2000)); const result = a + b; console.log('計算終了'); return result; } // 使い方 async function Calculator() { console.log('始めます'); const answer = await slowAdd(2, 3); console.log('答えは', answer); console.log('終わります'); }
実際のアプリケーションに例えると
日常生活に例えると、非同期処理はこのように理解できます:
// メイドさんにお茶を入れてもらう(同期処理) function getTeaNow() { console.log('お嬢様:メイドさん、お茶をお願いします'); console.log('メイド:すぐにお持ちいたします'); return '🫖 アールグレイ'; } // シェフにケーキを作ってもらう(非同期処理) async function orderCake() { console.log('お嬢様:シェフ、特製ケーキをお願いします'); console.log('シェフ:ご用意に30分ほど頂戴いたします'); // ケーキを作る時間(3秒)を表現 await new Promise(resolve => setTimeout(resolve, 3000)); console.log('シェフ:お待たせいたしました'); return '🍰 特製ショートケーキ'; } // 優雅なティータイム async function TeaParty() { console.log('お嬢様のティーパーティーの始まりです'); // ケーキを注文 const cake = await orderCake(); // お茶を用意 const tea = getTeaNow(); console.log(`${cake}と${tea}の準備が整いました`); }
ReactでこのティーパーティーのUIを実装すると:
async function CakeComponent() { const cake = await orderCake(); return <div>{cake}が届きました</div>; } function TeaComponent() { const tea = getTeaNow(); return <div>{tea}の準備ができました</div>; } export default function TeaParty() { return ( <div> <h1>お嬢様のティーパーティー</h1> <Suspense fallback={<div>ケーキを準備中でございます...</div>}> <CakeComponent /> </Suspense> <TeaComponent /> </div> ); }
まとめ
非同期処理、Promise、asyncとawaitの関係をまとめると:
- 非同期処理は、時間のかかる処理を行いながら、他の処理も実行できるようにする仕組み
- Promiseは「後で結果を返す約束」を表すオブジェクト
- asyncは「この関数は非同期処理を含みます」と宣言する修飾子
- awaitは「この処理が完了するまで待ちます」という指示
Next.jsのServer Componentでは、asyncとawaitを使うことで、非同期データ取得を簡潔に記述できます。また、Suspenseを組み合わせることで、ローディング状態も適切に扱えます。
これらの概念を理解することで、より効率的で使いやすいウェブアプリケーションを開発できるようになります。