🐹

ポケモンAPIを使ってGraphQLを今更覚える!

2021/08/19に公開

目的

有志の方が制作されているGraphQL のポケモン APIを使って
ポケモン検索アプリを作る過程でReact + TypeScript + GraphQLを覚える!

前述

本記事で作ったアプリはGitHubで公開しているので、よかったら記事を一緒に確認してください

事前準備

とりあえず React + TypeScript 環境を作る

npx create-react-app pokemon-graphql-practice --template typescript

たったのこれだけでReact + TypeScript環境が作成できる(神)

cd pokemon-graphql-practice
yarn start

不要なファイルは削除しましょう

  • src/App.css
  • src/App.text.tsx
  • src/logo.svg
  • src/setupTest.ts

App.tsxも不要な記述を消してとりあえずスッキリさせます

function App() {
  return <div>pokemon-graphql-practice</div>;
}
  • src/App.css

graphql-codegen で型定義を作る

GraphQLのSchema情報から型定義ファイルを作成してくれるすごいやつです

公式 Docsの Installation を参考にインストールする

yarn add graphql
yarn add -D @graphql-codegen/cli
yarn add -D @graphql-codegen/schema-ast // schema.graphqlを自動で作ってくれる!

セッティングを行う

yarn graphql-codegen init

色々聞かれるけどとりあえず全部Enterですっ飛ばしてOKです
script名を聞かれるのでそこだけ generate と入力します
graphql-codegenを実行するのに yarn generate で実行できるようになります
codegen.ymlが作成されるので、以下のように修正します

schema: "https://graphql-pokemon2.vercel.app" # GraphiQLを指定
generates:
  ./src/@types/types.d.ts:
    plugins:
      - "typescript"
      - "typescript-operations" # fqueryやmutationで使用するOperationとかの型定義まで作ってくれる
  ./schema.graphql: # これ以下を記述するとschema.graphqlをDocumentから作ってくれる(スゴイ)
    plugins:
      - schema-ast

ここまで来たらあとは実行するのみです!

yarn generate

以下のファイルが作成されます

  • ./src/@types/types.d.ts
  • ./schema.graphql

Apollo をインストール

Apolloはフロントエンドから簡単にGraphQLを操作できるようにするライブラリです

yarn add @apollo/client

実装

ここまで来たらもう準備万端!
早速実装に入りましょう!!!

...と言いたいところですが、その前にgraphql-pokemonが提供しているAPI仕様を確認しましょう

Pikachu の番号/名前/画像 url を取得する Query を試す

GraphiQLを開き、右上のDocsを開きます(Docsの見方は省略します)

query:Queryをクリックすると、queryとpokemonsとpokemonのFieldsがあることがわかります

スクリーンショット 2021-05-05 18.35.31.png

今回Pikachuのデータを取得したいので、pokemonを見てみると、pokemon(id: String,name: String): Pokemon とありますね

String型のidまたはnameを引数として渡すことで、Pokemon型のレスポンスを返却することがわかります

ではPokemon型の詳細を見てみましょう

スクリーンショット 2021-05-05 18.35.37.png

Pokemon型が持つFieldが表示されました
いろんなデータがありますがとりあえず今回必要なのは

  • number
  • name
  • image

ですね
これを元に左側のエディタでQueryを作成してみましょう!

スクリーンショット 2021-05-05 18.41.20.png

serachPikachu Queryを作りました
これを実行するとpikachuのデータが取れることがわかります

この"data"から始まるJSONが、クライアント側でQueryを実行した時のレスポンスとして返されます

GraphiQL超便利!!!

さてこれを実際にアプリで実行してみましょう!

Apollo の準備

src/graphql/client.tsを作成し、そこにApolloを使うためのクライアントを用意してあげます
ApolloClientを初期化します

import { ApolloClient, InMemoryCache } from "@apollo/client"

const apolloClient = new ApolloClient({
    uri: "https://graphql-pokemon2.vercel.app/",
    cache: new InMemoryCache(),
})

export { apolloClient }

uriにはGraphiQLを、cacheにはInMemoryCacheのインスタンスを渡します

cacheを設定することで同じクエリが発行された場合、自動的にキャッシュから結果を返却してくれるので、パフォーマンスがよくなるみたいです
とりあえず入れておいて損は無いかと思います

App.tsにApolloProviderと先ほど初期化したAppoloClientをインポートして、ApolloProviderでアプリ全体を囲ってあげます

import { ApolloProvider } from "@apollo/client";
import { apolloClient } from "./graphql/client";

function App() {
  return (
    <ApolloProvider client={apolloClient}>
      {" "}
      // 追加
      <div>pokemon-graphql-practice</div>
    </ApolloProvider> // 追加
  );
}

これでこのApolloProviderに囲われてるコンポーネント内で、GraphQLのqueryやmutationを実行できるようになります

結果を表示しよう

GraphQL APIを実行して結果を表示するコンポーネントを作っていきます

src/graphql/queries.tsに先ほどGraphQLで確認したQueryを記載しておきましょう

gqlにテンプレートリテラルでQueryを書くといい感じにリクエストを作ってくれるみたいです

import gql from "graphql-tag"

export const searchPikachu = gql`
    query searchPikachu {
        pokemon(name: "pikachu") {
            number
            name
            image
        }
    }
`

src/components/SearchResultField.tsxを作って、Queryを実行し結果を表示するコンポーネントを作ります

import { useQuery } from "@apollo/client"
import { Query } from "../@types/types"
import { searchPikachu } from "../graphql/queries"

const SearchResultField = () => {
    const { loading, error, data } = useQuery<Query>(searchPikachu)

    if (loading) return <>"Loading..."</>
    if (error) return <>`Error! ${error.message}`</>

    return (
        <div>
            <div>No: {data?.pokemon?.number}</div>
            <div>Name: {data?.pokemon?.name}</div>
            {data?.pokemon?.image ? (
                <img src={data?.pokemon?.image} alt={data?.pokemon?.name ?? ""} />
            ) : (
                <div>no image.</div>
            )}
        </div>
    )
}
export { SearchResultField }

useQueryにジェネリクスとして、dataの型を渡すことができます
@typesからQueryをとってきて渡すことで、ちゃんとdataがQuery型だとVSCodeが判定してくれました
当初の想定ではdataはPokemon型になると思っていましたが、dataはGraphiQLでの結果のJSON(以下)のようにpokemonキーが一番上にあったので、dataはQuery型にしました

{
  "data": {
    "pokemon": {
      ...
    }
  }
}

簡単に説明していきます

useQuery

import { useQuery } from "@apollo/client";
const { loading, error, data } = useQuery<Query>(searchPikachu);

Apolloが用意してくれているHooksです
通信の状況に応じてloading,error,dataの値が変わるのでとても便利

ハンドリング

if (loading) return <>Loading...</>;
if (error) return <>Error! {error.message}</>;

loadingがtrueの場合はローディング中の文字、エラー発生時にはメッセージを返却するようにしています

<></> これはフラグメントと行ってとりあえず囲みたい時に使います とても便利

データの表示

テストなので特別なハンドリングとかはせず、?.でごり押してます
imgだけsrcとaltにundifindを渡せなかったのでundifindの場合no image. が表示されるようにしました

return (
  <div>
    <div>No: {data?.pokemon?.number}</div>
    <div>Name: {data?.pokemon?.name}</div>
    {data?.pokemon?.image ? (
      <img src={data?.pokemon?.image} alt={data?.pokemon?.name ?? ""} />
    ) : (
      <div>no image.</div>
    )}
  </div>
);

ここまででPikachuのデータが表示されるようになりました

スクリーンショット 2021-05-06 1.25.01.png

動的にポケモンの名前が動的に設定できるようにしよう

ここまでで結果を取得、表示ができました
あとはPikachu以外のポケモンも調べられるようにしたいので、検索フォームを作って動的にポケモンを検索できるようにします

まずはGraphiQLで変数を使ったQueryを作成してみます

変数を使った Query

Queryに変数を渡すには以下のように記載します

スクリーンショット 2021-05-05 21.46.06.png
-2a9914945eb4.png)

左下のQUERY VARIABLESで変数にCharizardを入れて、それを左上のqueryで使用しています
検索結果が006 Charizardになったのがわかりますね

src/graphql/queries.tsに追加しておきましょう

export const searchPokemon = gql`
    query searchPokemon($name: String) {
        pokemon(name: $name) {
            number
            name
            image
        }
    }
`

検索フォームを作成

src/components/SearchForm.tsxを作成し、検索フォームを作っていきます

import { FormEvent, useRef } from "react"

type PropType = {
    setpokemonName: React.Dispatch<React.SetStateAction<string>>
}

const SearchForm: React.FC<PropType> = ({ setpokemonName }) => {
    const ref = useRef<HTMLInputElement>(null)

    const submitHandler = (e: FormEvent) => {
        e.preventDefault()
        if (ref !== null && ref.current !== null) {
            setpokemonName(ref.current.value)
        }
    }

    return (
        <form onSubmit={submitHandler}>
            <input type="text" ref={ref} placeholder="input Pokemon's name" />
            <input type="submit" value="Search"></input>
        </form>
    )
}

export { SearchForm }

src/App.tsxもこれに伴い修正

function App() {
    const [pokemonName, setpokemonName] = useState("")

    return (
        <> // 追加
            <SearchForm setpokemonName={setpokemonName}></ SearchForm> // 変更
            <ApolloProvider client={apolloClient}>
                <SearchResultField></SearchResultField>
            </ApolloProvider>
        </> // 追加
    )
}

Reactの解説記事では無いので詳細な説明は省きますが、

  • input:textをuseRef Hooksで定義
  • formのonSubmitでrefを参照し、valueを取得している
  • App.tsxでuseStateで作成したpokemonNameのセッター?を渡して、onSubmitでそこにvalueを突っ込む

って感じのフォームにしています
Submitボタンを押した時、テキストボックスの値がApp.tsxのpokemonNameに、入るようになりました

検索条件を動的な値に設定する

さて、検索フォームから受け取ったpokemonNameを使って検索結果を表示しましょう

// ↓追加
type PropType = {
    pokemonName: string
}

const SearchResultField: React.FC<PropType> = ({  pokemonName }) => { // 変更
    const { loading, error, data } = useQuery<Query> (searchPokemon, { variables: { name: pokemonName } }) // 変更

    if (!pokemonName) return <></> // 追加
    if (loading) return <>Loading...</>
    if (error) return <>Error! {error.message}</>
    if (!data || !data.pokemon) return <>No Data.</> // 追加

    // ↓変更
    return (
        <>
            <div>No: {data.pokemon.number}</div>
            <div>Name: {data.pokemon.name}</div>
            {data.pokemon.image ? <img src={data.pokemon.image} alt={data.pokemon.name ?? ""} /> : <div>no image.</div>}
        </>
    )
}

useQuery<Query> (searchPokemon, { variables: { name: pokemonName } })
useQueryにsearchPokemonを第一引数で渡し、第二引数にvariablesを保持したObjectを渡しています
このvariablesには先ほどGraphiQLの左下でのQUERY VARIABLESで確認した物と同じ型のオブジェクトを渡します
useQueryは変数として渡したパラメータが変更された時に再度Queryを実行してくれるので、これで検索条件を可変にできました

最後にApp.tsxからpokemonNameを渡します
ついでにpokemonNameが存在しない場合は何も表示しないようにしました

<ApolloProvider client={apolloClient}>
    {pokemonName && <SearchResultField pokemonName={pokemonName}> // 変更</SearchResultField>}
</ApolloProvider>

これでSubmitが押される度に検索結果が更新されるようになりました!

pokemon-qiita-mov.gif

まとめ

React + TypeScript + GraphQLでポケモンの画像検索ができる簡易的なアプリを作成しました

大好きなポケモンでGraphQLを勉強出来て、graphql-pokemonの作者には感謝してもしきれません
が、欲を言うなら全ポケモンに対応して欲しい・・・あと日本語・・・

GraphQLの勉強のついでに作ってみましたが、GraphiQL最高すぎるし、graphql-code-generatorで型定義ファイルが作れるの天才だし、Apolloがとても使いやすくて、簡単でした

GraphiQLのお陰でREST APIより簡単にお試しできるので、とてもとっつきやすかったです
ただGraphQLじゃないと出来ないことっていうのがそこまで内容に見受けられました
最近流行りのgRPCとかだとRESTではできないことが出来たりするみたいです
とても気に入ったのでGraphQLには是非これから流行って欲しいですが、gRPCも勉強して流れに乗り遅れないようにしないといけないなと思いました・・・

今回はqueryしか使っていませんが(graphql-pokemonにはqueryしか用意されていません)
GitHub APIとかにはmutationもあるのでmutationの記事もそのうち書きたいです
バックエンドのGraphQLの書き方も勉強しないと・・・

参考

graphql-pokemon

graphql-pokemon の GraphiQL

graphql-code-generator 公式 Docs

GraphQL Code Generator の使い方。〜GraphQL の Schema から TS の型定義を自動生成する〜

Apollo Docs

GitHubで編集を提案

Discussion