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>;  
}

このコードはエラーになります。なぜなら:

  1. fetchはPromiseを返す
  2. awaitがないのでPromiseの結果を待たない
  3. そのため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の関係をまとめると:

  1. 非同期処理は、時間のかかる処理を行いながら、他の処理も実行できるようにする仕組み
  2. Promiseは「後で結果を返す約束」を表すオブジェクト
  3. asyncは「この関数は非同期処理を含みます」と宣言する修飾子
  4. awaitは「この処理が完了するまで待ちます」という指示

Next.jsのServer Componentでは、asyncとawaitを使うことで、非同期データ取得を簡潔に記述できます。また、Suspenseを組み合わせることで、ローディング状態も適切に扱えます。

これらの概念を理解することで、より効率的で使いやすいウェブアプリケーションを開発できるようになります。