🐥

ポケモンAPIアプリの解説【React】

2024/08/26に公開

https://hama-pokeapi.netlify.app/
https://github.com/hamanyann/pokeAPI

本記事について

この記事ではポケモンAPIアプリを紹介、コード解説します。
・Reactを勉強していて何か作りたい人
・APIを習ったけど何が出来るかわからない人
・ポケモンAPIで何が出来るか知りたい人
上記のような初級者の方を対象にしています。

TypeScriptとtailwindcssを使っていますが、知らなくても伝わるように努めます。

完成品

カウントアプリ


現在のカウントに対応するポケモンの「画像・名前・種族値」をポケモンAPIから取得します。

クイズアプリ


ランダムに選択されたポケモンの「画像・名前」を利用して英語名を当てるクイズです。

ポケモンAPI

https://pokeapi.co/
https://qiita.com/jinto/items/c953ab25253d8ec82e30
参考URL:ポケモンAPIとポケモンAPIの取得データの詳細

上記の2つの理解出来れば解説は必要ないのですが、初級者向けにAPIと併せて簡単に説明します。

https://pokeapi.co/api/v2/pokemon/
上記のURLがポケモンAPIの本体。
URLからデータを取得して何かをするのがAPIを使うということ。


これがAPIの中身です。

もしくは、

上記のようなページ表示の方もいるかもしれません。

2つの表示の違いは「JSON形式か、そうでないか」です。
上の画像はJSON形式。
下の画像は生のAPIデータ。

chromeブラウザでJSON形式で表示するには下記の拡張機能を入れてみてください。
https://chromewebstore.google.com/detail/jsonview/gmegofmjomhknnokphhckolhcffdaihd

話は戻りまして、JSON形式で説明します。


count next previous results などは覚えなくていいです。
このAPIはこの名前でobjectの中にこの関数名でデータが入っている、ということです。

nextは次の20匹のURLです。
一度に全部のポケモンデータは取得できず、20匹ずつで取得する仕様になっているようです。

そして、results配列の中のnameurl がポケモンのデータです。
一つ目の bulbassaur がフシギダネの英語名です。
urlをクリックするとフシギダネの詳細ページに飛びます。

https://pokeapi.co/api/v2/pokemon/1/

一応補足するとurlの最後の数字の1はフシギダネの図鑑Noです。
なのでhttps://pokeapi.co/api/v2/pokemon/2/ は進化後のフシギソウのURLです。

フシギダネのURLをクリックしてもらうとものすごい量のコードが出てきて「うぇっ」となりますが、これだけのデータを利用できるということでもあります。

参考URLを見るとどんなデータがあるか載っています。
また、https://pokeapi.co/api/v2/pokemon-species/1/
上記のURLに日本語名の「フシギダネ」があるのでそれを使えば日本語名に変換も出来るようです。

今回は初級者向けということでわかりやすいデータの取得をしていきます。

TOPページ

App.tsx
import Count from './count/Count';
import { useState } from 'react';
import Quiz from './quiz/Quiz';

function App() {
  const [showCount, setShowCount] = useState(false);
  const [showQuiz, setShowQuiz] = useState(false);

  const handleToggleCount = () => {
    setShowCount(!showCount);
  };

  const handleToggleQuiz = () => {
    setShowQuiz(!showQuiz);
  };

  return (
    <>
      {!showCount && !showQuiz && (
        <div className="flex flex-col items-center">
          <p className="text-3xl mt-20">ポケモンAPIアプリ</p>
          <div className="flex justify-center items-center mt-20 font-bold">
            <button
              className="bg-red-500 w-[150px] h-[150px] mr-4 "
              onClick={handleToggleCount}
            >
              カウントアプリSTART
            </button>
            <button
              className="bg-green-500 w-[150px] h-[150px]"
              onClick={handleToggleQuiz}
            >
              クイズアプリSTART
            </button>
          </div>
        </div>
      )}

      {showCount && <Count onClick={handleToggleCount} />}

      {showQuiz && <Quiz onClick={handleToggleQuiz} />}
    </>
  );
}

export default App;

どこからどこまで説明するか迷うところですが、補足としてclassName="flex flex-col items-centerという書き方はtailwindcssというCSSのフレームワークです。
https://tailwindcss.com/

今回の趣旨から外れるので詳細は割愛しますが、CSSを直接書いているだけなので気にしないでください。

onClickshowCount``showQuizのtrue/faulseを切り替えています。

ここの解説としてはそれくらいですね。

カウントアプリ

Count.tsx
import { useState } from 'react';
import Pokemon from '../views/Pokemon';
import PokemonStatus from '../views/PokemonStatus';

const Count = ({ onClick }: { onClick: () => void }) => {
  const [count, setCount] = useState(1);

  const increment = (value: number) => {
    setCount(count + value);
  };

  const decrement = (value: number) => {
    setCount(count - value);
  };

  return (
    <>
      <button
        onClick={onClick}
        className="text-2xl border-2 border-black p-1 w-30 mt-4 ml-4"
      >
        TOP</button>
      <div className="flex flex-col items-center">
        <h1 className="text-3xl mt-4">カウントアプリ(Max 1025</h1>
        <div className="flex items-center ">
          <Pokemon count={count} />
          <PokemonStatus count={count} />
        </div>
        <h1 className="text-4xl m-8 mt-2">{count}</h1>
        <div className="flex flex-none">
          <div className="flex flex-col items-start gap-2 mr-10">
            <button
              className="text-2xl border-2 border-black p-2 w-20"
              onClick={() => increment(1)}
            >
              +1
            </button>
            <button
              className="text-2xl border-2 border-black p-2 w-20"
              onClick={() => increment(10)}
            >
              +10
            </button>
            <button
              className="text-2xl border-2 border-black p-2 w-20"
              onClick={() => increment(100)}
            >
              +100
            </button>
          </div>
          <div className="flex flex-col items-start gap-2 ">
            <button
              className="text-2xl border-2 border-red-500 p-2 w-20"
              onClick={() => decrement(1)}
            >
              -1
            </button>
            <button
              className="text-2xl border-2 border-red-500 p-2 w-20"
              onClick={() => decrement(10)}
            >
              -10
            </button>
            <button
              className="text-2xl border-2 border-red-500 p-2 w-20"
              onClick={() => decrement(100)}
            >
              -100
            </button>
          </div>
        </div>
      </div>
    </>
  );
};

export default Count;


基本的には普通のカウントアプリです。

説明をする部分としては下記ですね。

 <div className="flex items-center ">
    <Pokemon count={count} />
    <PokemonStatus count={count} />
 </div>

Pokemon コンポーネントとPokemonStatus にcountを渡しています。
このcountは表示されている現在のカウントです。

ポケモンAPIの取得(カウントアプリ)

Pokemon.tsx

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

interface PokemonProps {
  count: number;
}

const Pokemon = ({ count }: PokemonProps) => {
  const [pokemon, setPokemon] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const getPokemonUrl = () => {
    setLoading(true);

    fetch(`https://pokeapi.co/api/v2/pokemon/${count}`)
      .then(res => res.json())
      .then(json => {
        setPokemon(json.sprites.front_default);
        setLoading(false);
      });
  };

  useEffect(() => {
    getPokemonUrl();
  }, [count]);

  return (
    <>
      {loading ? (
        <div className="w-[96px] h-[96px] flex items-center justify-center">
          Loading...
        </div>
      ) : pokemon ? (
        <>
          <img className="w-[96px] h-[96px]" src={pokemon} />
         
        </>
      ) : (
        <p>no image...</p>
      )}
    </>
  );
};

export default Pokemon;

ここが本題のAPI取得ですね。

const getPokemonUrl = () => {
    setLoading(true);

    fetch(`https://pokeapi.co/api/v2/pokemon/${count}`)
      .then(res => res.json())
      .then(json => {
        setPokemon(json.sprites.front_default);
        setLoading(false);
      });
  };

この部分でAPIを取得しています。

最初に伝えたように、URLの最後の数字がポケモン図鑑Noに対応しています。

なので受け取った現在のcountをURLの最後に直接書き込むことで、対応するポケモンのデータを取得しています。

他にはmap関数ですべてのデータを取得して何番目、というやり方もあるかと思いますが、自分は直接URLいじったほうがわかりやすかったです。

.then(res => res.json())は受け取った生のデータをJSON形式にしています。

setPokemon(json.sprites.front_default);はjson形式にしたデータの、spritesの中のfront_defaultを取得しています。

https://pokeapi.co/api/v2/pokemon/1/

上記のフシギダネのデータを開いてfront_defaultを検索してみください。

spritesオブジェクトのfront_defaultが今回取得している画像です。

試しに、setPokemon(json.sprites.front_default);setPokemon(json.sprites.other.dream_world.front_default);に変更すると、下記のように取得画像が変わります。


ドリームワールド(ブラウザゲーム)の画像は上記の部分ですね。

このようにAPIを読み解けば好きなデータを自由に使うことが出来ます。

あとはローディングが終わったらsetLoading(false);にしてローディングの文字を消す、つまり画像の取得が終わるまでローディングの文字を表示しています。

PokemonStatus.tsx

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

interface PokemonProps {
  count: number;
}

const PokemonStatus = ({ count }: PokemonProps) => {
  const [pokemonName, setPokemonName] = useState<string | null>(null);
  const [pokemonStatus, setPokemonStatus] = useState<number[]>([]);

  const getPokemonUrl = () => {
    fetch(`https://pokeapi.co/api/v2/pokemon/${count}`)
      .then(res => res.json())
      .then(json => {
        setPokemonName(json.name);
        setPokemonStatus(
          json.stats.map((stat: { base_stat: number }) => stat.base_stat)
        );
      });
  };

  useEffect(() => {
    getPokemonUrl();
  }, [count]);

  return (
    <div className='ml-10 mt-2'>
      <div className="flex flex-col items-center text-xl">{pokemonName}</div>
      <div className="flex flex-col  ">HP{pokemonStatus[0]}</div>
      <div className="flex flex-col  ">
        攻撃:{pokemonStatus[1]}
      </div>
      <div className="flex flex-col  ">
        防御:{pokemonStatus[2]}
      </div>
      <div className="flex flex-col ">
        特攻:
        {pokemonStatus[3]}
      </div>
      <div className="flex flex-col  ">
        特防:{pokemonStatus[4]}
      </div>
      <div className="flex flex-col  ">
        素早:{pokemonStatus[5]}
      </div>
    </div>
  );
};

export default PokemonStatus;

ステータス取得の<PokemonStatus>も中身はほぼ一緒です。

 const getPokemonUrl = () => {
    fetch(`https://pokeapi.co/api/v2/pokemon/${count}`)
      .then(res => res.json())
      .then(json => {
        setPokemonName(json.name);
        setPokemonStatus(
          json.stats.map((stat: { base_stat: number }) => stat.base_stat)
        );
      });
  };

setPokemonStatus( json.stats.map((stat: { base_stat: number }) => stat.base_stat) );
この部分でステータスを取得しています。

注意点としては、画像を取得した際はjson.sprites.front_defaultでいけましたが、ステータスは中身が配列[]になっています。
[hp,attack,defense・・・]
のような形ですね。

なのでmap関数で個々をstatという関数に置き換えて、statのbase_statを取得して、新しい配列[45,49,49,65,65,45]を作成しています。

ちなみにstat: { base_stat: number }はTypeScriptの書き方でbase_statは数字ですよという型宣言というものです。

クイズアプリ

Quiz.tsx
import PokemonQuiz from '../views/PokemonQuiz';
import Pokemon from '../views/Pokemon';
import { useEffect, useState } from 'react';

const array: number[] = [];

for (let i = 1; i <= 151; i++) {
  array.push(i);
}

const shuffleArray = (array: number[]) => {
  for (let i = array.length - 1; i > 0; i--) {
    const randomIndex = Math.floor(Math.random() * (i + 1));
    [array[i], array[randomIndex]] = [array[randomIndex], array[i]];
  }
  return array
};

const Quiz = ({ onClick }: { onClick: () => void }) => {
  const [quiz1, setQuiz1] = useState(1);
  const [quiz2, setQuiz2] = useState(1);
  const [quiz3, setQuiz3] = useState(1);
  const [quiz4, setQuiz4] = useState(1);
  const [answer, setAnswer] = useState(1);
  const [correct, setCorrect] = useState('');

  const restQuiz = () => {
    const shuffledArray = shuffleArray(array);
    setQuiz1(shuffledArray[0]);
    setQuiz2(shuffledArray[1]);
    setQuiz3(shuffledArray[2]);
    setQuiz4(shuffledArray[3]);
    const randomCorrect = Math.floor(Math.random() * 4);
    setAnswer(shuffledArray[randomCorrect]);
    setCorrect('');
  };

  useEffect(() => {
    restQuiz();
  }, []);

  const handelClick = (number: number) => {
    if (number === answer) {
      setCorrect('ゲット!');
    } else {
      setCorrect('逃げられた...');
    }
  };

  return (
    <>
      <button
        onClick={onClick}
        className="text-2xl border-2 border-black p-1 w-30 mt-4 ml-4"
      >
        TOP</button>
      <div className="flex flex-col items-center">
        <p className="text-3xl mt-10">名前を呼んでゲットしよう!</p>
        <Pokemon count={answer} />
        <PokemonQuiz count={quiz1} onClick={() => handelClick(quiz1)} />
        <PokemonQuiz count={quiz2} onClick={() => handelClick(quiz2)} />
        <PokemonQuiz count={quiz3} onClick={() => handelClick(quiz3)} />
        <PokemonQuiz count={quiz4} onClick={() => handelClick(quiz4)} />
        <p className="text-3xl pt-10">{correct}</p>
        <button
          onClick={restQuiz}
          className="text-2xl border-2 border-black p-1 w-30 mt-4 ml-4"
        >
          次のポケモンに進む
        </button>
      </div>
    </>
  );
};
export default Quiz;

クイズアプリの解説です。

const array: number[] = [];

for (let i = 1; i <= 151; i++) {
  array.push(i);
}

const shuffleArray = (array: number[]) => {
  for (let i = array.length - 1; i > 0; i--) {
    const randomIndex = Math.floor(Math.random() * (i + 1));
    [array[i], array[randomIndex]] = [array[randomIndex], array[i]];
  }
  return array
};

上記のコードでは151の数字をランダムな並びにしています。
[23,45,12,5,35・・・]といった感じです。

  const [quiz1, setQuiz1] = useState(1);
  const [quiz2, setQuiz2] = useState(1);
  const [quiz3, setQuiz3] = useState(1);
  const [quiz4, setQuiz4] = useState(1);
  const [answer, setAnswer] = useState(1);
  const [correct, setCorrect] = useState('');

初期値を1にしているので、開いた直後にフシギダネがちらっと出てきます。
初期値を0にしたら「URLが存在しません」というエラーが出るのでとりあえず1を初期値にしました。

const restQuiz = () => {
    const shuffledArray = shuffleArray(array);
    setQuiz1(shuffledArray[0]);
    setQuiz2(shuffledArray[1]);
    setQuiz3(shuffledArray[2]);
    setQuiz4(shuffledArray[3]);
    const randomCorrect = Math.floor(Math.random() * 4);
    setAnswer(shuffledArray[randomCorrect]);
    setCorrect('');
  };

  useEffect(() => {
    restQuiz();
  }, []);

先ほど作成した[23,45,12,5,35・・・]の配列の前から4つを選択しに当てはめてます。
そして4つの中からランダムに一つをanswerにセットしています。

この処理の理由としては下記です。
・同じ数字がランダムに選ばれないように151の数字から4つ選ぶ形にしたかったため
・答えの場所が固定されないようにするため

const handelClick = (number: number) => {
    if (number === answer) {
      setCorrect('ゲット!');
    } else {
      setCorrect('逃げられた...');
    }
  };

解答ボタンを押した際に、押した答えが合ってるか判定しています。

<>
      <button
        onClick={onClick}
        className="text-2xl border-2 border-black p-1 w-30 mt-4 ml-4"
      >
        TOP</button>
      <div className="flex flex-col items-center">
        <p className="text-3xl mt-10">名前を呼んでゲットしよう!</p>
        <Pokemon count={answer} />
        <PokemonQuiz count={quiz1} onClick={() => handelClick(quiz1)} />
        <PokemonQuiz count={quiz2} onClick={() => handelClick(quiz2)} />
        <PokemonQuiz count={quiz3} onClick={() => handelClick(quiz3)} />
        <PokemonQuiz count={quiz4} onClick={() => handelClick(quiz4)} />
        <p className="text-3xl pt-10">{correct}</p>
        <button
          onClick={restQuiz}
          className="text-2xl border-2 border-black p-1 w-30 mt-4 ml-4"
        >
          次のポケモンに進む
        </button>
      </div>
    </>

TOPボタンはTOPページに戻るボタン。

<Pokemon count={answer} />これはカウントアプリで使ったポケモンの画像を取得するコンポーネントですね。
これにanswerにセットされた図鑑Noをpropsで渡してます。

<PokemonQuiz count={quiz1} onClick={() => handelClick(quiz1)} />は次で説明します。

上記のPokemonQuizコンポーネントに、ランダムに選ばれた図鑑Noをpropsで渡している、ということです。

ポケモンAPIの取得(クイズアプリ)

PokemonQuiz
import { useEffect, useState } from 'react';

interface PokemonProps {
  count: number;
  onClick: () => void;
}

const PokemonQuiz = ({ count, onClick }: PokemonProps) => {
  const [pokemonName, setPokemonName] = useState<string | null>(null);

  const getPokemonUrl = () => {
    fetch(`https://pokeapi.co/api/v2/pokemon/${count}`)
      .then(res => res.json())
      .then(json => {
        setPokemonName(json.name);
      });
  };

  useEffect(() => {
    getPokemonUrl();
  }, [count]);

  return (
    <>
      <div className="flex flex-col items-center ">
        <button className="text-3xl border-2 border-black p-2 w-60" onClick={onClick}>
          {pokemonName}
        </button>
      </div>
    </>
  );
};

export default PokemonQuiz;

これもほぼ同じですね。

setPokemonName(json.name);で単純に名前を取得しています。

<PokemonQuiz count={quiz1} onClick={() => handelClick(quiz1)} />で与えられた図鑑Noのポケモンの名前ですね。

まとめ

以上になります。
自分もAPIを最初にudemyやyoutubeで学んだときは何言ってるかわからなかったので、いろんな人や記事から情報を得て、作っているうちに理解してくると思います。

自分もまだまだ勉強中なので一緒に頑張りましょう!

Discussion