🤡

【React】useEffectとPokéAPI で学ぶ非同期処理

に公開
「非同期処理」って?

同期処理とは:
普通のコードは上から順番に1つずつ実行される。

console.log('1');
console.log('2');
console.log('3');

結果:

1
2
3

非同期処理とは:
すぐに終わらないもの(時間がかかるもの)をやってる間に、他の処理を先に進める仕組みのこと。

例:

  • サーバーからデータを取ってくる(API通信)
  • ファイルを読み込む
  • タイマー(setTimeout)
console.log('1');
setTimeout(() => {
  console.log('2');
}, 1000);
console.log('3');

結果:

1
3
2(1秒後)

→「setTimeout」が終わるのを待たずに、次(3)が先に実行される。

なぜ非同期処理なのか

例えば、ネットワークからデータを取ってくるとき、数秒~数十秒かかることがあるため、もし同期でやったら画面が固まってしまう。

そのため、以下が重要。

  • 非同期にして待つ
  • その間にUIの更新や別の処理を進める
JavaScriptの非同期の方法
  1. コールバック(昔のやり方)
function fetchData(callback: (data: string) => void) {
  setTimeout(() => {
    callback('データ取得完了!');
  }, 1000);
}

fetchData((data) => {
  console.log(data);
})

問題点:

  • コールバックがネストが深くなりやすい(いわゆるコールバック地獄)
  • エラーハンドリングが面倒
  1. Promise(現在の標準)
function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('データ取得完了!');
    }, 1000);
  });
}

fetchData().then((data) => {
  console.log(data);
});
  • resolve: 成功したときの処理
  • reject: 失敗したときの処理

エラー処理:

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });
  1. async / await
async function getData() {
  const data = await fetchData();
  console.log(data);
}

getData()

ポイント

  • async を関数につけると、その中で await が使える
  • await はPromiseの結果を「待つ」

APIをReactとuseEffectを使って叩いてみる(TypeScript)

PokéAPIという無料APIからデータを取ってくるとする。


↑こんな感じ

ファイル構造

my-app/
├── src/
├── components/
│ └── PokemonComponent.tsx
├── App.tsx
└── index.tsx

サンプルコード

PokemonComponent.tsx
import React, { useEffect, useState } from 'react';

type Pokemon = {
  name: string;
  sprites: {
    front_default: string;
  };
  types: {
    type: {
      name: string;
    };
  }[];
};

const PokemonComponent = () => {
  const [pokemon, setPokemon] = useState<Pokemon | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchPokemon = async () => {
      console.log('1. データ取得開始');

      try {
        const res = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu');
        console.log('2. レスポンス受け取り完了');

        if (!res.ok) throw new Error('取得に失敗しました');

        const data: Pokemon = await res.json();
        console.log('3. JSON 変換完了');
        console.log('4. データの中身:', data);

        setPokemon(data);
        console.log('5. ステートにセット完了');
      } catch (err) {
        console.error('エラー発生:', err);
        setError((err as Error).message);
      }
    };

    fetchPokemon();
    console.log('6. fetchPokemon 呼び出し完了');
  }, []);

  if (error) return <p>エラー: {error}</p>;
  if (!pokemon) return <p>ポケモン読み込み中...</p>;

  return (
    <div>
      <h2>{pokemon.name.toUpperCase()}</h2>
      <p>タイプ: {pokemon.types[0].type.name}</p>
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
    </div>
  );
};

export default PokemonComponent;

setTimeoutを使って、もっと非同期を体感する。

サンプルコード

PokemonComponent.tsx
import React, { useEffect, useState } from 'react';

type Pokemon = {
  name: string;
  sprites: {
    front_default: string;
  };
  types: {
    type: {
      name: string;
    };
  }[];
};

const PokemonComponent = () => {
  const [pokemon, setPokemon] = useState<Pokemon | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchPokemon = async () => {
      console.log('1. データ取得開始');

      // 2秒待つ
      await new Promise((resolve) => {
        console.log('2. 2秒待機開始');
        setTimeout(() => {
          console.log('3. 2秒経過');
          resolve(true);
        }, 2000);
      });

      try {
        const res = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu');
        console.log('4. レスポンス受け取り完了');

        if (!res.ok) throw new Error('取得に失敗しました');

        const data: Pokemon = await res.json();
        console.log('5. JSON変換完了');
        console.log('6. データ:', data);

        setPokemon(data);
        console.log('7. ステートにセット完了');
      } catch (err) {
        console.error('エラー:', err);
        setError((err as Error).message);
      }
    };

    fetchPokemon();
    console.log('8. fetchPokemon呼び出し完了');
  }, []);

  if (error) return <p>エラー: {error}</p>;
  if (!pokemon) return <p>ポケモン読み込み中...</p>;

  return (
    <div>
      <h2>{pokemon.name.toUpperCase()}</h2>
      <p>タイプ: {pokemon.types[0].type.name}</p>
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
    </div>
  );
};

export default PokemonComponent;

参考

参考にさせていただきました🙏
https://qiita.com/yunity29/items/7ccc84d47e139340ecbc
https://zenn.dev/nameless_sn/articles/javascript_async_tutorial
https://qiita.com/asahina820/items/665c55594cfd55e6f14a

Discussion