🐹

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

2021/08/19に公開約11,200字

目的

有志の方が制作されている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

Discussion

ログインするとコメントできます