こんにちは。エンジニアの本山です。
私はstakに入社して GraphQL を触り始めました。
まだまだGraphQLを理解したといえるレベルには達していないので、自身のキャッチアップも兼ねて、graphql-pokemon を使って簡単なポケモン検索アプリを作り、GraphQL に入門したいと思います。
主な使用技術
- GraphQL
- Next.js
- TypeScript
- Apollo Client
はじめに
本記事では、GraphQL の基礎と一部実装例を紹介します。
GraphQL を扱う上での基礎的な内容ですので、実際のプロダクト開発等では活用できない場合があります。
そのことを予めご了承ください。
GraphQL概要
GraphQLとは、API向けに作られたクエリ言語及びクエリを実行するサーバーサイドの実装のことです。
、、、分かりづらいですね。ざっくりまとめると「クエリ言語とスキーマ言語で構成された API の規格」といったところでしょうか。
これまた分かりづらいですね。
より理解しやすくするために、GraphQLの一部特徴を紹介します。
- アプリケーションが呼び出すエンドポイントが 1 つ
- 柔軟なリクエストで最小限のレスポンス
- 1 回のリクエストで複数のリソースにアクセス可能
- スキーマによる型付けにより、堅牢な開発ができる
このように GraphQL は、柔軟性、開発者にとっての使いやすさを向上させるために設計されていることが分かると思います。
より正確で詳細な情報がほしい方は、ぜひ GraphQL公式サイト を読んでいただけたらと思います。
Next.js + TypeScript導入
では実装に入っていきたいと思います。
まずは Next.js + TypeScript を導入します。
npx create-next-app pokemon-search-graphql --template typescript
この1コマンドだけで Next.js + TypeScript 環境が構築できるのは最高です。
とりあえずサーバーを立ち上げて、Welcom画面を確認します。
next dev
問題無くWelcom画面が表示されていますね。
GraphQL + Apollo Client導入
Apollo Client とは GraphQL をクライアント側で使うためのライブラリです。
状態管理の機能も兼ね備えています。
yarn add @apollo/client graphql
上記コマンドで GraphQL と Apollo Client を導入します。
src/graphql/apolloClient.ts を作成し、そこに Apollo Client を使うためのクライアントを用意します。
import {ApolloClient, InMemoryCache} from "@apollo/client"
export const client = new ApolloClient({
uri: "https://graphql-pokemon2.vercel.app",
cache: new InMemoryCache()
})
uri には、ポケモンAPIのURLを設定。
cache には InMemoryCache のインスタンスを渡します。
cache を設定することで同じクエリが発行された場合、自動的にキャッシュから結果を返却してくれるので、パフォーマンスがよくなるみたいです。
index.tsx に ApolloProvider と先ほど初期化した AppoloClient をインポートし、ApolloProvider でアプリ全体を囲ってあげます。
import type {NextPage} from "next"
import {ApolloProvider} from "@apollo/client"
import {client} from "../graphql/apolloClinet"
const Home: NextPage = () => {
return (
<ApolloProvider client={client}>
<>content</>
</ApolloProvider>
)
}
export default Home
ApolloProvider に囲われてるコンポーネント内で、GraphQL の Query や Mutation を実行することができるようになります。
ポケモンAPIの確認
実装の前にポケモンAPI の仕様を確認したいと思います。
Altair GraphQL Client を開き、ポケモンAPI のURLを入力します。
次に右上の Docs を開き、Query をクリックします。
query、pokemons、pokemon の field があることが確認できました。
今回は pokemon を使用します。
まずは pokemon の arguments を見てみましょう。
pokemon(id: String, name: String): Pokemon
とありますね。
String 型の id または name を引数として渡すことで、Pokemon 型のレスポンスが返ってくることがわかります。
次にPokemon 型の詳細を見てみましょう。
Pokemon型 の field が表示されました。
今回必要なデータは、
- name
- image
ですね。
さっそく Query を実行してみましょう。
例として、arguments に Charmander を入れて実行してみます。
Charmander のデータ取得に成功しました。
次にアプリで実行していきましょう。
検索フォームの作成
src/component/PokemonSearchForm.tsx を作成し、検索フォームを作っていきます。
import {FormEvent, useRef} from "react"
type Props = {
setPokemonName: (name: string) => void
}
export const PokemonSearchForm = ({setPokemonName}: Props) => {
const ref = useRef<HTMLInputElement>(null)
const handleOnSubmit = (e: FormEvent) => {
e.preventDefault()
if (ref !== null && ref.current !== null) {
setPokemonName(ref.current.value)
}
}
return (
<form onSubmit={handleOnSubmit}>
<input type="text" ref={ref} />
<input type="submit" value="検索"></input>
</form>
)
}
検索ボタンを押した時に、テキストボックスの値を、関数の引数に渡しております。
src/pages/index.tsx で、検索フォームをインポートします。
検索フォームに渡す関数を定義し、フォームから渡された name を useState で保持するようにします。
import type {NextPage} from "next"
import {ApolloProvider} from "@apollo/client"
import {client} from "../graphql/apolloClinet"
import {useState} from "react"
import {PokemonSearchForm} from "../component/PokemonSearchForm"
const Home: NextPage = () => {
const [value, setValue] = useState<string>('')
const setPokemonName = (name: string) => {
setValue(name)
}
return (
<>
<PokemonSearchForm setPokemonName={setPokemonName} />
<ApolloProvider client={client}>
{value}
</ApolloProvider>
</>
)
}
export default Home
とりあえず検索フォームに入力した値を、フォーム下に表示させるのみです。
検索フォームは出来たので、次に入力した値でQueryを実行できるようにします。
検索結果を表示する
src/component/PokemonSearchResultField.tsx を作成し、Query を実行して結果を表示するコンポーネントを作っていきます。
import {gql, useQuery} from "@apollo/client"
import Image from "next/image"
type Props = {
name: string
}
type Query = {
pokemon: {
name: string
image: string
}
}
export const PokemonSearchResultField = ({name}: Props) => {
const pokemonDocument = gql`
query Pockemon($name: String) {
pokemon(name: $name) {
name
image
}
}
`
const {data, error, loading} = useQuery<Query>(pokemonDocument, {
variables: {
name
}
})
if (!name) return <></>
if (loading) return <>読込中...</>
if (error) return <>エラー!</>
if (!data || !data.pokemon) return <>検索結果がありません</>
return (
<div>
<p>{`名前:${data.pokemon.name}`}</p>
<Image src={data.pokemon.image} alt={`${data.pokemon.name}-image`} width={250} height={250} />
</div>
)
}
useQuery等を簡単に説明していきます。
useQuery
Apollo が用意してくれている hooks です。
コンポーネントが render されたタイミングでクエリが実行され、通信の状況に応じて loading, error, data の値が変わります。
const {data, error, loading} = useQuery<Query>(pokemonDocument, {
variables: {
name
}
})
pokemonDocument は、gql にテンプレートリテラルで Query を書いたものです。
variables には、arguments を設定します。フォームから渡ってくる値を variables に渡しています
。useQuery は変数として渡した値が変更された時に、再度 Query を実行してくれます。
useQuery にジェネリクスとして、data の型を渡すことが出来、先程 Altair で実行した結果を参考に、Query タイプを定義しました。
ハンドリング
if (!name) return <></>
if (loading) return <>読込中...</>
if (error) return <>エラー!</>
if (!data || !data.pokemon) return <>検索結果がありません</>
各状況に応じて、表示文字を変更するようにしています。フォームに何も入力されていない場合は、何も表示しません。
データの表示
return (
<div>
<p>{`名前:${data.pokemon.name}`}</p>
<Image src={data.pokemon.image} alt={`${data.pokemon.name}-image`} width={250} height={250} />
</div>
)
名前のところにpokemon.name、Imageコンポーネントにpokemon.imageを渡しています。
動作確認
それでは index.tsx に検索結果表示コンポーネントをインポートして、動作確認を行ってみたいと思います。
PokemonSearchResultField.tsx に useState で保持しているフォームの値を渡します。
検索ボタンを押すたびに、値は可変します。
import type {NextPage} from "next"
import {ApolloProvider} from "@apollo/client"
import {client} from "../graphql/apolloClinet"
import {useState} from "react"
import {PokemonSearchForm} from "../component/PokemonSearchForm"
import {PokemonSearchResultField} from "../component/PokemonSearchResultField"
const Home: NextPage = () => {
const [value, setValue] = useState<string>('')
const setPokemonName = (name: string) => {
setValue(name)
}
return (
<>
<PokemonSearchForm setPokemonName={setPokemonName} />
<ApolloProvider client={client}>
<PokemonSearchResultField name={value} />
</ApolloProvider>
</>
)
}
export default Home
想定通り検索に応じて、検索結果が表示されるようになりました!
これでポケモン検索アプリの完成です!
まとめ
Next.js + TypeScript + GraphQL で簡単なポケモン検索アプリを作成しました!
ポケモンAPIに限らずですが、誰でも簡単に GraphQL を勉強できる環境を提供しているポケモンAPIの作者には、感謝しかありません。
GraphQL を触ってみて、REST より簡単に試すことが出来て、入門しやすいのかなと個人的に思いました。それと、フロントエンドライクな感じがして、単純に叩いていて楽しいなとも感じました。
今回は Query のみでしたが、Mutation を使用したアプリの作成、GraphQL Code Generator を使って型の自動生成を試したり、GraphQL に関する記事を今後も書いていきたいなと思います。