vivit株式会社でフロントエンドエンジニアをしている関です。
新型コロナウイルス感染症(COVID-19)への対策として発出された緊急事態宣言の影響で、弊社も2ヶ月ほどリモートワークとなり、私も駆け足で自宅に作業環境を構築しました。買っちゃいましたL字デスク。
さて、弊社ではいくつかのプロダクトのフロントエンドに React(Next.js) + TypeScript を採用しており、バックエンドとの通信には GraphQL(Apollo) を採用しています。
今回はその中で、コンポーネント分割をする際の GraphQL Query の定義と取得したデータの受け渡しについて話をします。
ここからは GitHub GraphQL API v4 を使って説明していきます。
アジェンダ
pages
からcomponents
へのデータの受け渡し方法について- Fragment で分割する問題点
graphql-anywhere
が凄い
前提
この記事で利用している各技術の詳細な説明は省きます。
pages から components へのデータの受け渡し方法について
Next.js でページを作成する際、コンポーネントを components
ディレクトリに分割して置くのが一般的かと思いますが、 GraphQL API で取得したデータを TypeScript の型定義に則って子コンポーネントで利用する際にどうやろうというのが問題になり、やり方の候補がいくつか挙がりました。
1. コンポーネントが必要な項目の取得を全てページで行う
メリット
- データの受け渡しがいたってシンプル
デメリット
- ページとコンポーネントが密結合になる
- ページ側の記述が膨れる
// pages/index.tsx const query = gql` query Index { search(query: "github", first: 10, type: REPOSITORY) { nodes { ... on Repository { id nameWithOwner updatedAt } } } } `; export default () => { const { data, loading } = useQuery(query); const nodes = data?.search.nodes || []; if (loading) return <>Loading...</>; return ( <> <Head> <title>Segmented fragment</title> <link rel="icon" href="/favicon.ico" /> </Head> <Repos repos={nodes} /> </> ); };
ページ内で使う項目をすべてページ側で定義、取得しコンポーネントに渡しています。
これでは子コンポーネント内で使う詳細な項目の知識までページが持ってしまっているので、子コンポーネントの変更にページが引っ張られる形になってしまいます。密です。
2. コンポーネントが必要なデータの取得を自身で行う
メリット
デメリット
- コンポーネントがそれぞれデータを取得するので通信回数が増える
// components/Repos/index.tsx const query = gql` query Index { search(query: "github", first: 10, type: REPOSITORY) { nodes { ... on Repository { id nameWithOwner updatedAt } } } } `; export const Repos = () => { const { data, loading } = useQuery(query); const repos = data?.search.nodes || []; const list = repos.map((repo) => ( <tr key={repo.id}> <td>{repo.nameWithOwner}</td> <td>{repo.updatedAt}</td> </tr> )); return ( <table> <thead> <tr> <th>Name</th> <th>Updated at</th> </tr> </thead> <tbody>{list}</tbody> </table> ); };
コンポーネント内に必要な項目を取得するクエリを書き、またそれを実行しています。
GraphQL の利点を正面からぶっ潰すパワーのある案です。
3. コンポーネントに必要なクエリを Fragment で記述、取得はページで行う
メリット
- ページがコンポーネントに項目まで依存しない
- データの取得回数も一度で済む
デメリット
- そのままだとページが取得したデータをまるごとコンポーネントに渡すことになる
// components/Repos/index.graphql fragment Repos on Query { search(query: "github", first: 10, type: REPOSITORY) { nodes { ...Nodes } } } fragment Nodes on Repository { id nameWithOwner updatedAt }
// components/Repos/index.tsx type Props = { result: ReposFragment; }; export const Repos = ({ result }: Props) => { const repos = result.search.nodes as NodesFragment[]; const list = repos.map((repo) => ( <tr key={repo.id}> <td>{repo.nameWithOwner}</td> <td>{repo.updatedAt}</td> </tr> )); return ( <table> <thead> <tr> <th>Name</th> <th>Updated at</th> </tr> </thead> <tbody>{list}</tbody> </table> ); };
// pages/index.graphql query Index { ...Repos }
// pages/index.tsx export default () => { const { data, loading } = useIndexQuery(); if (loading) return <>Loading...</>; return ( <> <Head> <title>Segmented fragment</title> <link rel="icon" href="/favicon.ico" /> </Head> <Repos result={data} /> </> ); };
この例ではコンポーネントで使う項目を Fragment で定義し、そこから GraphQL Code Generator で生成された **Fragment
型を props の型に定義しています。
※ Fragment について Queries and Mutations | GraphQL
話は逸れますが、コンポーネント名のディレクトリを切った中に index.tsx
と index.graphql
を分けて定義し、クエリの実行には自動生成された Hooks を使うことによりクエリとコードの切り分けを行っています。詳しい実装は最後に記載するサンプルプロジェクトで参照できます。
例示用に使っている GitHub GraphQL API v4 のスキーマを TypeScript の型定義に落とし込む都合上コンポーネント側のクエリがゴチャっていますが、こういう場合にゴチャッとした部分をコンポーネント側に閉じ込められるのもメリットと言えますね。
さて一見良さそうですが一つ難点があります。
Fragment で分割する問題点
滅茶苦茶 度々メルカリの記事やコードを参考にしているのですが、見かけた記事内でこのようなやりとりを見つけました。
(何ならこの記事のほとんどは以下の記事に書かれていました)
フロントエンドからのGraphQLクエリを最適化した by vvakame · Pull Request #134 · mercari/mtc2018-web
取得したデータをそれぞれのコンポーネントに丸々渡しているのはやはり気持ち悪いと思うようで、 graphql-anywhere
という名前を出していました。
graphql-anywhere が凄い
実際には filter
という graphql-anywhere
内のユーティリティメソッドなのですが、 GraphQL Code Generator で同じく生成された gql タグ(下記の例では ReposFragmentDoc
)を元に取得したデータをフィルタリングすることができます。
import { filter } from "graphql-anywhere"; import Head from "next/head"; import * as React from "react"; import { Repos } from "../components"; import { ReposFragment, ReposFragmentDoc, useIndexQuery } from "../graphql"; export default () => { const { data, loading } = useIndexQuery(); if (loading) return <>Loading...</>; return ( <> <Head> <title>Segmented fragment</title> <link rel="icon" href="/favicon.ico" /> </Head> <Repos result={filter<ReposFragment>(ReposFragmentDoc, data)} /> </> ); };
実は Apollo の公式ドキュメントでも graphql-anywhere
を使ったフィルタリング方法が書かれています。 本稿では TypeScript のジェネリクスで動的に型を指定しています。
実際に動作するプロジェクトを作成したので公開します。
動作には GITHUB_TOKEN
という名前で OAuth トークンを環境変数に設定する必要があります。
ドキュメントに従って取得、設定をしてください。