urqlをさわってみるぞ
はじめに
この記事は、urqlについて調べたことをまとめた記事です。
以下、調べようと思ったモチベーションです。urqlが気になっているかたの参考になれば幸いです。
- Apollo Client 以外のGraphQLクライアントをを試してみたかった
- urqlのドキュメントキャッシュがよさそう、という記事を読んだので試してみたかった
- Apollo Clientからurqlに乗り換えた、という記事が話題になっていたので気になっていた
urqlとは
まずはドキュメントを見てみます。カスタマイズ性と汎用性の高さが推されているようです。
urqlとは
The highly customizable and versatile GraphQL client for React, Svelte, Vue, or plain JavaScript, with which you add on features like normalized caching as you grow.
- highly customizable(高度にカスタマイズが可能)
- versatile(汎用性が高い)
- GraphQL client
早速さわってみる
早速動かしてみます。urql は React、Svelte、Vue、またはプレーンJavaScriptで動かせるようです。今回は React を使ってみます。
以下の構成で動かしてみます。
- React(Next.js)
- CSRでブラウザからリクエストする
- GraphQLサーバーに Supabaseを利用する
- graphql-codegen でコード生成する
事前準備
フロントから呼び出す GraphQLサーバーの準備が必要です。以前 Supabaseのローカル環境でGraphQLサーバーを動かした記事を書いたことがあったので、この時の環境を利用します。
事前に準備するQueryとMutation
-
上記の記事で、以下のQueryとMutationを用意しています。
-
Query
query Query {
employeesCollection {
edges {
node {
id
name
}
}
}
}
- Mutation
# Operation
mutation Mutation($objects: [employeesInsertInput!]!) {
insertIntoemployeesCollection(objects: $objects) {
records {
id
name
}
}
}
# Variables
{
"objects": [
{
"name": "太郎"
}
]
}
urqlを導入するフロントの環境の準備。create next-app
します
$ yarn create next-app -- --ts
yarn create v1.22.19
warning From Yarn 1.0 onwards, scripts don't require "--" for options to be forwarded. In a future version, any explicit "--" will be forwarded as-is to the scripts.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Installed "create-next-app@12.2.5" with binaries:
- create-next-app
✔ What is your project named? … urql-next-app
インストール
- 必要なパッケージをインストールする
$ yarn add urql graphql graphql-tag
- 初期化する関数を作成する
- supabaseの api を利用するので
.env
にNEXT_PUBLIC_ANON_KEY
を追記して headersに含めるようにしています。 src/lib/graphql.ts
import { createClient } from 'urql';
export const client = createClient({
url: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:54321/graphql/v1',
fetchOptions: () => {
const token = process.env.NEXT_PUBLIC_ANON_KEY;
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
apikey: token ? token : '',
},
};
},
});
- Providerに追加する
_app.tsx
import type { AppProps } from 'next/app'
import { Provider } from 'urql';
import { client } from '../lib/graphql';
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider value={client}> // 追記
<Component {...pageProps} />
</Provider>
);
}
export default MyApp
Queryを追加する
-
src/graphql/FetchEmployeeList.graphql
を作成する
query FetchEmployeeList {
employeesCollection {
edges {
node {
...employees
}
}
}
}
- graphql-codegen を導入する
$ yarn add -D @graphql-codegen/cli
$ yarn graphql-codegen init
yarn graphql-codegen init 実行結果
$ yarn graphql-codegen init
yarn run v1.22.19
$ /Users/shimabukuromeg/develop/src/github.com/shimabukuromeg/urql-next-app/node_modules/.bin/graphql-codegen init
Welcome to GraphQL Code Generator!
Answer few questions and we will setup everything for you.
? What type of application are you building? Application built with React
? Where is your schema?: (path or url) http://localhost:54321/graphql/v1
? Where are your operations and fragments?: graphql/**/*.graphql
? Pick plugins: TypeScript (required by other typescript plugins), TypeScript Operations (operations and fragments), Urql Introspection (for Urql Client)
? Where to write the output: generated/graphql.ts
? Do you want to generate an introspection file? No
? How to name the config file? codegen.yml
? What script in package.json should run the codegen? generate
Fetching latest versions of selected plugins...
Config file generated at codegen.yml
$ npm install
To install the plugins.
$ npm run generate
To run GraphQL Code Generator.
✨ Done in 201.01s.
-
yarn generate
して scheme からコードを自動生成する - 失敗した。code-genで supabaseのスキーマをロードする際に、アクセス権がなくて失敗してそう
$ yarn generate (git)-[main]
yarn run v1.22.19
$ graphql-codegen --config codegen.yml
✔ Parse Configuration
⚠ Generate outputs
❯ Generate generated/graphql.ts
✖
Failed to load schema from http://localhost:54321/graphql/v1:
-
.env
から変数を受け取れるようにyarn generate
のコマンド修正する
~ 省略
"scripts": {
~ 省略
"generate": "graphql-codegen --config codegen.yml -r dotenv/config"
},
- code-gen の ドキュメントで environment-variables について記載されてるページ
-
codegen.yml
を修正してheaderに必要な情報を追加する(NEXT_PUBLIC_BASE_URL
とNEXT_PUBLIC_ANON_KEY
は.env
に記載)
overwrite: true
schema:
- ${NEXT_PUBLIC_BASE_URL}:
headers:
Authorization: "Bearer ${NEXT_PUBLIC_ANON_KEY}"
apikey: ${NEXT_PUBLIC_ANON_KEY}
documents: "src/graphql/**/*.graphql"
generates:
generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "urql-introspection"
- configの書き方について、ドキュメント参考ページ
- あらためて
yarn generate
する
$ yarn generate
yarn run v1.22.19
$ graphql-codegen --config codegen.yml
✔ Parse Configuration
✔ Generate outputs
✨ Done in 4.25s.
-
src/generated/graphql.ts
が生成された。成功。 -
urqlのプラグイン追加する。
codegen.yml
の pluginsにtypescript-urql
の追記もして、再度yarn generate
する。
$ yarn add -D @graphql-codegen/typescript-urql
$ yarn generate
- code-genのurql のプラグインについて、ドキュメント参考ページ
- code-gen で生成された Query のフックを使う
import { useFetchEmployeeListQuery } from '@/generated/graphql';
const [result, reexecuteQuery] = useFetchEmployeeListQuery();
const { data, fetching, error } = result;
- urql query の書き方について、ドキュメント参考ページ
- useQuery について、ドキュメント参考ページ
Mutationを作成する
-
続いて、Mutationを追加する
-
src/graphql/CreateEmployee.graphql
mutation CreateEmployee($objects: [employeesInsertInput!]!) {
insertIntoemployeesCollection(objects: $objects) {
records {
id
name
}
}
}
-
yarn generate
する -
code-gen で生成された Mutation のフックを使う
import { useCreateEmployeeMutation } from '@/generated/graphql';
// hooks
const [
insertIntoemployeesCollectionResult,
insertIntoemployeesCollection
] = useCreateEmployeeMutation()
// 更新処理
insertIntoemployeesCollection({ objects: [value] }).then(result => {
if (result.error) {
console.error('Oh no!', result.error);
}
});
- urql mutation の書き方について、ドキュメント参考ページ
- useMutation について、ドキュメント参考ページ
Exchangeを使って機能拡張してみる
Exchangeとは
-
urqlの機能拡張する時に使われる仕組み
-
リクエストをインターセプトすることでクライアントの機能を拡張する
-
Apollo の Linkや Redux における middleware のような仕組み
-
createClient する時に exchangesオプションを指定しない場合、デフォルトで以下の3つが指定される
- dedupExchange
- cacheExchange
- fetchExchange
-
ドキュメントの参考ページ(設計についてまとめられてるページ)
例えば、ウィンドウがフォーカスを取り戻したときにQueryを再フェッチする拡張機能を追加したい場合は、ドキュメントの以下のページに導入方法が記載されています。
手順にしたがって導入してみます。
- パッケージインストールする
$ yarn add @urql/exchange-refocus
-
src/lib/graphql.ts
を編集してcreateClient
のexchanges
オプションに、インストールした拡張機能を追加して完了です。
src/lib/graphql.ts 最終系
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
import { refocusExchange } from '@urql/exchange-refocus';
export const client = createClient({
url: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:54321/graphql/v1',
// @ts-ignore
exchanges: [dedupExchange, refocusExchange(), cacheExchange, fetchExchange],
requestPolicy: 'cache-first',
fetchOptions: () => {
const token = process.env.NEXT_PUBLIC_ANON_KEY;
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
apikey: token ? token : '',
},
};
},
});
導入後、挙動確認してみたさいに、window切り替えても再フェッチされなそう(?)だったんですが、これは正しい挙動で、タブが変わって戻ってきた際に再フェッチされるようでした
完成
最後にMUIで少しスタリングしてみた完成系がこちらです。queryで取得した employeesの一覧がが表示されています。
以上、簡単な queryとmutation、拡張機能(exchange)を導入してみただけですが、urql + graphql-codegen + supabase を組み合わせて動かすことができました。
素振りしたソースコード
ドキュメントキャッシュ
冒頭で、ドキュメントキャッシュ良さそうだから試してみたいと書きました。さわってみた感想を書きます。
最初に良さそうと思ったのは、こちらの記事を読んだ感想でした。
そもそもドキュメントキャッシュとは
まず、ドキュメントキャッシュとは
- urqlが採用してるキャッシュの概念
- クエリとクエリに渡される変数を元にハッシュ化されたキーを使い、クエリの結果をキャッシュして管理する仕組み
- キャッシュが更新されるのは、Mutation実行時に結果に含まれる
__typename
に反応して、同じ__typename
を持つクエリのキャッシュを再取得した時に更新される - ドキュメントの図がとてもわかりやすいです
リクエストポリシーについては4パターン
- cache-first(デフォルト。キャッシュがあったらキャッシュを使い、なかったリクエストして取得する)
- cache-and-network (キャッシュを使いつつ、キャッシュがあってもなくてもリクエストして取得する)
- network-only(キャッシュを使わない。常にリクエストして取得する)
- cache-only (常にキャッシュを使う。リクエストして取得しない)
素振りしたサンプルの例
以下、今回動かしてみたサンプルで Mutationを動かしてる様子ですが、employees 登録の Mutation 後にキャッシュを更新するコードを特別書いていないにも関わらず、登録後に画面に反映されてるのがわかると思います。
これは Mutationの結果に含まれる __typename
と、employees 一覧のクエリに含まれる __typename
が同じなので、urqlが自動でデータを再取得してくれているからです。
注意事項としては、元々のデータが空っぽの場合は、__typename
が含まれていないので、Mutationを実行しても再フェッチされません。
以下、データがない状態でemployees登録のMutationを叩いてみると、データが再取得されてないのがわかると思います。
この挙動に関して、落とし穴だから気をつけてね、とドキュメントにも記載されており、事前にタイプを追加するもしくは正規化さえたキャッシュを使うようにするやり方が紹介されていました。
- キャッシュの落とし穴について、ドキュメント参考ページ
- 最初に取得したデータが空データでも
__typename
が入るようにadditionalTypenamesを Query のフックに追加修正
const context = React.useMemo(
() => ({ additionalTypenames: ["employees"] }),
[]
);
const [result] = useFetchEmployeeListQuery({
context,
});
ドキュメントキャッシュ良さそうだから試してみたいの感想
データ追加や更新時に、正規化されたデータのキャッシュ管理をするのは、複雑になりがちなので再フェッチしてデータ取得しなおすことはあると思います。
このようなケースが多く、正規化されたキャッシュが必要ない場合は、ドキュメントキャッシュ使うと、シンプルに管理できて良さそうだと思いました。
ドキュメントキャッシュの仕組みについて、swrなどもパスなどの文字列をキーにしてキャッシュ管理してるあたりが雰囲気近そうだと思いました。urqlはハッシュ化したキーを自動で作る分便利そう。
Apolloからurqlに乗り換えるモチベーション
以前、Apollo Clientからurqlに乗り換えた、という記事が話題になっていたのであらためて読んでみました。
気になっていたのはこちらの記事です。
この記事では urqlの以下のポイントが推されているようでした。
- ドキュメントが良い
- プラグインやパッケージでの追加機能のサポートをファーストパーティーで提供されてる
- キャッシュの管理が簡単で効果的
- ローカル状態を管理する機能を提供していない
- 拡張機能(Exchange)の仕組みがわかりやすい
- Next.js サポートプラグインの提供
個人的には、この記事に書かれている Apollo Clientに対する辛みを感じきれていないところはありますが、urqlの推しポイントがわかったのが良かったです。
さわってみた感じ、urqlのドキュメントは確かにシンプルでわかりやすい印象でした。ベースとなるコア機能をシンプルに提供して、必要に応じて機能拡張する方法を公式から出してるのは良さそうだと思いました。
比較
apollo client、relay、urql のGraphql Client ついて、それぞれの比較がまとめられてるhasuraの記事がわかりやすかったです。
おわりに
- ドキュメントキャッシュがシンプルなキャッシュの仕組みになっているので、複雑なキャッシュ管理が不要の場合は採用すると管理が楽そうで良さそう
- ドキュメントが読みやすかった
- Exchangeの仕組みが便利で良かった。ベースとなるコア機能をシンプルに提供して、必要に応じて機能拡張していくという考え方すごくいいなと思った
- ドキュメントの冒頭にあったように、highly customizable(高度にカスタマイズが可能)で versatile(汎用性が高い)な GraphQL Client だということがわかって勉強になった
参考
Discussion