ポケモンAPIアプリの解説【React】
本記事について
この記事ではポケモンAPIアプリを紹介、コード解説します。
・Reactを勉強していて何か作りたい人
・APIを習ったけど何が出来るかわからない人
・ポケモンAPIで何が出来るか知りたい人
上記のような初級者の方を対象にしています。
TypeScriptとtailwindcssを使っていますが、知らなくても伝わるように努めます。
完成品
カウントアプリ
現在のカウントに対応するポケモンの「画像・名前・種族値」をポケモンAPIから取得します。
クイズアプリ
ランダムに選択されたポケモンの「画像・名前」を利用して英語名を当てるクイズです。
ポケモンAPI
参考URL:ポケモンAPIとポケモンAPIの取得データの詳細
上記の2つの理解出来れば解説は必要ないのですが、初級者向けにAPIと併せて簡単に説明します。
URLからデータを取得して何かをするのがAPIを使うということ。
これがAPIの中身です。
もしくは、
上記のようなページ表示の方もいるかもしれません。
2つの表示の違いは「JSON形式か、そうでないか」です。
上の画像はJSON形式。
下の画像は生のAPIデータ。
chromeブラウザでJSON形式で表示するには下記の拡張機能を入れてみてください。
話は戻りまして、JSON形式で説明します。
count next previous results
などは覚えなくていいです。
このAPIはこの名前でobjectの中にこの関数名でデータが入っている、ということです。
next
は次の20匹のURLです。
一度に全部のポケモンデータは取得できず、20匹ずつで取得する仕様になっているようです。
そして、results
配列の中のname
と url
がポケモンのデータです。
一つ目の bulbassaur がフシギダネの英語名です。
urlをクリックするとフシギダネの詳細ページに飛びます。
一応補足するとurlの最後の数字の1はフシギダネの図鑑Noです。
なのでhttps://pokeapi.co/api/v2/pokemon/2/ は進化後のフシギソウのURLです。
フシギダネのURLをクリックしてもらうとものすごい量のコードが出てきて「うぇっ」となりますが、これだけのデータを利用できるということでもあります。
参考URLを見るとどんなデータがあるか載っています。
また、https://pokeapi.co/api/v2/pokemon-species/1/
上記のURLに日本語名の「フシギダネ」があるのでそれを使えば日本語名に変換も出来るようです。
今回は初級者向けということでわかりやすいデータの取得をしていきます。
TOPページ
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のフレームワークです。
今回の趣旨から外れるので詳細は割愛しますが、CSSを直接書いているだけなので気にしないでください。
onClick
でshowCount``showQuiz
のtrue/faulseを切り替えています。
ここの解説としてはそれくらいですね。
カウントアプリ
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
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を取得しています。
上記のフシギダネのデータを開いてfront_defaultを検索してみください。
spritesオブジェクトのfront_defaultが今回取得している画像です。
試しに、setPokemon(json.sprites.front_default);
をsetPokemon(json.sprites.other.dream_world.front_default);
に変更すると、下記のように取得画像が変わります。
ドリームワールド(ブラウザゲーム)の画像は上記の部分ですね。
このようにAPIを読み解けば好きなデータを自由に使うことが出来ます。
あとはローディングが終わったらsetLoading(false);
にしてローディングの文字を消す、つまり画像の取得が終わるまでローディングの文字を表示しています。
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は数字ですよという型宣言というものです。
クイズアプリ
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の取得(クイズアプリ)
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