【GraphQL】剣盾世代までの簡易なポケモンずかんを作ってみた
概要
業務で使ったGraphQLをおさらいしたいと思っていたら、ポケモンのデータに関するAPI、pokeAPIがbeta的にGraphQLに対応しているようだったので、それで何か作ってみることにしました。
もともとはgraphql-pokemonという、これまたどストレートなAPIがあったので、それでやろうとしていたのですが、結局以下のメリットからpokeAPIを選びました。
- 画像がゲームの見た目そのままだった
- 今回はゲームの中でもらえる図鑑に雰囲気を寄せたかった。graphql-pokemonのポケモン画像はとてもきれいで、ゲームで見るよりもう少し滑らかな見た目だった
- 取得できる情報が多かった。たとえばポケモンの後ろ向きの画像なども取れた
- 「第3世代のポケモン」みたいな取得の仕方ができた
pokeAPIの方は、リクエストが1時間に100回まで、データは剣盾までしかない、などの制限もあったのですが、遊びで作ってるだけだしその辺はよいだろうということで、あまり気にしませんでした。
完成品gif
完成品リポジトリ
使った技術
- GraphQL
- urql
- React(Next.js)
- tailwind
- react-paginate
実装のピックアップ
作った中で、ここはこういう工夫をしました、というのを書きます。
データの取得
pokeAPIのGraphQLの叩き方は少し特殊で、queryを叩く際に、返ってくるデータの種類をfilterしたい場合はこんな感じで、引数にwhereを入れてあげる、という形式を取る必要がありました。
const getPokemons = gql(`
query getPokemons {
pokemon_v2_pokemon(limit:30, where:{pokemon_species_id: {_gt: 500}}) {
name
id
pokemon_v2_pokemonsprites {
sprites
}
}
}
`)
_gtはgreater thanの略で、上記は、ポケモンのidが500よりも大きいものを対象にする、という条件になります。
また、今回GraphQL Clientはurqlを使いました。
今回API実装は自前ではなく外部のものであったため、mutationもなく、当然複雑なキャッシュ等も存在しなかったため、なるべくシンプルなclientでよいだろう、と思ったのが理由です。
検索条件の1つを選んでもらったタイミングではrefetchはしたくなかったので、useQueryの引数にあった、pauseという「queryを走らせるか否か?」のboolを使って、セレクトボックスのhandleChangeが動くとそこをfalseにして、最後検索ボタンを押したタイミングでreexecuteを実行するようにしました。
複雑なことはしていないので、当然と言えば当然なのですが、urqlは非常にシンプルで使いやすかったです。
ちなみに逸れますが、GraphQL Clientは何を選定すればよいのか?を考えるには、こちらの記事がわかりやすかったです。
今回はクライアントでの状態管理は求めていないので、graphql-requestでもよかったかもしれませんが、まあよしとしましょう🙄
ページネーションの見た目をモンスターボール風にした
こちらの記事を参考にさせていただき、ページネーションコンポーネントのうち、今選ばれているページはモンスターボール風の見た目になるようにしました。
今回使っているreact-paginateでは、JSXをpropsに渡してページネーションの見た目を調整する、ということがむずかしそうだったので、コンポーネント内で使われているliなどのピュアな要素にどうにかstyleをあてる必要がありました。
また、Next.jsではどうやら、classやidが付与されているわけではない、ただのliなどの要素をmodule cssで操作するのは不可のようだったので、global cssで雑に装飾しました。
見た目はこんな感じです。
ここでは4ページ目が開かれているので、4番目のコンポーネントがモンスターボール風になっています。
cssはこちらです。
globals.css
@import "~tailwindcss/base";
@import "~tailwindcss/components";
@import "~tailwindcss/utilities";
ul {
display: flex;
justify-content: center;
font-size: 16px;
gap: 12px;
}
li {
list-style-type: none;
cursor: pointer;
border-radius: 50%;
}
li a {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid black;
border-radius: 30px;
color: black;
font-weight: bold;
}
.selected {
background: linear-gradient(red 46%, black 46%, black 54%, white 54%);
}
英語データを日本語に変換
APIから返ってくる文字データ(今回は名前, 特性)は全て英語だったので、日本語に変換しました。
こちらの記事を参考にさせていただき、wikipediaにポケモンの名前の英語・日本語対応表がある、ということを知ったので、とりあえずその表をcsvにし、csv to jsonで調べて出てきたその辺のサイトでJSON化して扱いました。
API側は先頭が大文字で、文字間はハイフンで表現しており、JSON側は全て小文字で、文字間は半角スペースで表現していたため、toLowerCaseやreplaceを使って比較可能な状態にしました。
最初はcsvtojsonというライブラリを使って手元でJSON化していたのですが、、、
Vercelに上げるとfilePath関連で怒られたので、もう初めからJSONにしとこうと思ってこうした次第です。
検索方法の充実
pokeAPIは、取得の際いろんなfilterの仕方が可能だったので、めざといものをピックアップして、検索条件として使いました。
具体的には、「世代」「タイプ」「件数」で検索できるようにしました。
上でもお見せしたgifですが、イメージはこんな感じです。
こちらのgifでは、世代は第3、タイプはほのお、件数は10で条件を絞っています。
実際のqueryの内容はこんな感じで、引数にはそれぞれ、セレクトボックスを押してsetされたstateを少し加工して入れています。
const getPokemons = gql(`
query getPokemons($limit: Int!, $pokemonType: [String!], $_gt: Int!, $_lt: Int!) {
pokemon_v2_pokemon(limit: $limit, where:{pokemon_species_id: {_gt: $_gt, _lt: $_lt}, pokemon_v2_pokemontypes: { pokemon_v2_type: { name: { _in: $pokemonType}}}}) {
name
id
pokemon_v2_pokemonsprites {
sprites
}
}
}
`)
セレクトボックスの「なし」が選択されているときは、その項目はfilterしたくなかったので、その条件でqueryを投げても全てのデータが返ってくるような値を設定して、引数に渡しました。
たとえば、タイプの条件で「なし」が選択されているときは、引数のpokemonTypeに全てのタイプを入れてあげて、タイプでは実質絞り込みが行われないようにしました。
作ったもののリンク
ポケモン図鑑サイト: https://pokemon-graphql-eight.vercel.app/
サイトはレスポンシブ対応していないので、スマホで見ると崩れます orz
また、1時間に100回叩くとAPIに制限が入るので、タイミングによっては正常に閲覧できない方もいらっしゃると思います。あしからず・・・。
所感
GraphQLはほぼqueryしただけで、mutationも裏のAPI作りも行っていないですが、GraphiQL等も使って、なんとなく取得の仕方はこんな感じだったなーというのを思い出せたので、よかったです。
参考記事
Discussion