🙆

urqlをさわってみるぞ

2022/08/22に公開

はじめに

この記事は、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

https://formidable.com/open-source/urql/docs/

https://github.com/FormidableLabs/urql

早速さわってみる

早速動かしてみます。urql は React、Svelte、Vue、またはプレーンJavaScriptで動かせるようです。今回は React を使ってみます。

以下の構成で動かしてみます。

  • React(Next.js)
  • CSRでブラウザからリクエストする
  • GraphQLサーバーに Supabaseを利用する
  • graphql-codegen でコード生成する

事前準備

フロントから呼び出す GraphQLサーバーの準備が必要です。以前 Supabaseのローカル環境でGraphQLサーバーを動かした記事を書いたことがあったので、この時の環境を利用します。

https://zenn.dev/shimabukuromeg/articles/097d93b021f28a

事前に準備する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 を利用するので .envNEXT_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 について記載されてるページ

https://www.graphql-code-generator.com/docs/config-reference/codegen-config#environment-variables

  • codegen.yml を修正してheaderに必要な情報を追加する(NEXT_PUBLIC_BASE_URLNEXT_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の書き方について、ドキュメント参考ページ

https://www.graphql-code-generator.com/docs/config-reference/schema-field#supported-configuration

  • あらためて 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 のプラグインについて、ドキュメント参考ページ

https://www.graphql-code-generator.com/plugins/typescript/typescript-urql

  • code-gen で生成された Query のフックを使う
import { useFetchEmployeeListQuery } from '@/generated/graphql';

const [result, reexecuteQuery] = useFetchEmployeeListQuery();
const { data, fetching, error } = result;
  • urql query の書き方について、ドキュメント参考ページ

https://formidable.com/open-source/urql/docs/basics/react-preact/#queries

  • useQuery について、ドキュメント参考ページ

https://formidable.com/open-source/urql/docs/api/urql/#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 の書き方について、ドキュメント参考ページ

https://formidable.com/open-source/urql/docs/basics/react-preact/#mutations

  • useMutation について、ドキュメント参考ページ

https://formidable.com/open-source/urql/docs/api/urql/#usemutation

Exchangeを使って機能拡張してみる

Exchangeとは

  • urqlの機能拡張する時に使われる仕組み

  • リクエストをインターセプトすることでクライアントの機能を拡張する

  • Apollo の Linkや Redux における middleware のような仕組み

  • createClient する時に exchangesオプションを指定しない場合、デフォルトで以下の3つが指定される

    • dedupExchange
    • cacheExchange
    • fetchExchange
  • ドキュメントの参考ページ(設計についてまとめられてるページ)

https://formidable.com/open-source/urql/docs/architecture/#the-client-and-exchanges

例えば、ウィンドウがフォーカスを取り戻したときにQueryを再フェッチする拡張機能を追加したい場合は、ドキュメントの以下のページに導入方法が記載されています。

https://formidable.com/open-source/urql/docs/api/refocus-exchange/

手順にしたがって導入してみます。

  • パッケージインストールする
$ yarn add @urql/exchange-refocus
  • src/lib/graphql.ts を編集して createClientexchanges オプションに、インストールした拡張機能を追加して完了です。
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切り替えても再フェッチされなそう(?)だったんですが、これは正しい挙動で、タブが変わって戻ってきた際に再フェッチされるようでした

https://github.com/FormidableLabs/urql/issues/2268

完成

最後にMUIで少しスタリングしてみた完成系がこちらです。queryで取得した employeesの一覧がが表示されています。

以上、簡単な queryとmutation、拡張機能(exchange)を導入してみただけですが、urql + graphql-codegen + supabase を組み合わせて動かすことができました。

素振りしたソースコード

https://github.com/shimabukuromeg/urql-next-app

ドキュメントキャッシュ

冒頭で、ドキュメントキャッシュ良さそうだから試してみたいと書きました。さわってみた感想を書きます。

最初に良さそうと思ったのは、こちらの記事を読んだ感想でした。

https://zenn.dev/adwd/articles/f4c5c5120467bb

そもそもドキュメントキャッシュとは

まず、ドキュメントキャッシュとは

  • urqlが採用してるキャッシュの概念
  • クエリとクエリに渡される変数を元にハッシュ化されたキーを使い、クエリの結果をキャッシュして管理する仕組み
  • キャッシュが更新されるのは、Mutation実行時に結果に含まれる __typename に反応して、同じ __typename を持つクエリのキャッシュを再取得した時に更新される
  • ドキュメントの図がとてもわかりやすいです

https://formidable.com/open-source/urql/docs/basics/document-caching/

リクエストポリシーについては4パターン

  • cache-first(デフォルト。キャッシュがあったらキャッシュを使い、なかったリクエストして取得する)
  • cache-and-network (キャッシュを使いつつ、キャッシュがあってもなくてもリクエストして取得する)
  • network-only(キャッシュを使わない。常にリクエストして取得する)
  • cache-only (常にキャッシュを使う。リクエストして取得しない)

素振りしたサンプルの例

以下、今回動かしてみたサンプルで Mutationを動かしてる様子ですが、employees 登録の Mutation 後にキャッシュを更新するコードを特別書いていないにも関わらず、登録後に画面に反映されてるのがわかると思います。
これは Mutationの結果に含まれる __typename と、employees 一覧のクエリに含まれる __typename が同じなので、urqlが自動でデータを再取得してくれているからです。

注意事項としては、元々のデータが空っぽの場合は、__typename が含まれていないので、Mutationを実行しても再フェッチされません。

以下、データがない状態でemployees登録のMutationを叩いてみると、データが再取得されてないのがわかると思います。

この挙動に関して、落とし穴だから気をつけてね、とドキュメントにも記載されており、事前にタイプを追加するもしくは正規化さえたキャッシュを使うようにするやり方が紹介されていました。

  • キャッシュの落とし穴について、ドキュメント参考ページ

https://formidable.com/open-source/urql/docs/basics/document-caching/#adding-typenames

  • 最初に取得したデータが空データでも __typename が入るようにadditionalTypenamesを Query のフックに追加修正
  const context = React.useMemo(
    () => ({ additionalTypenames: ["employees"] }),
    []
  );

  const [result] = useFetchEmployeeListQuery({
    context,
  });

ドキュメントキャッシュ良さそうだから試してみたいの感想

データ追加や更新時に、正規化されたデータのキャッシュ管理をするのは、複雑になりがちなので再フェッチしてデータ取得しなおすことはあると思います。
このようなケースが多く、正規化されたキャッシュが必要ない場合は、ドキュメントキャッシュ使うと、シンプルに管理できて良さそうだと思いました。
ドキュメントキャッシュの仕組みについて、swrなどもパスなどの文字列をキーにしてキャッシュ管理してるあたりが雰囲気近そうだと思いました。urqlはハッシュ化したキーを自動で作る分便利そう。

Apolloからurqlに乗り換えるモチベーション

以前、Apollo Clientからurqlに乗り換えた、という記事が話題になっていたのであらためて読んでみました。

気になっていたのはこちらの記事です。

https://blog.logrocket.com/why-i-finally-switched-to-urql-from-apollo-client/

この記事では urqlの以下のポイントが推されているようでした。

  • ドキュメントが良い
  • プラグインやパッケージでの追加機能のサポートをファーストパーティーで提供されてる
  • キャッシュの管理が簡単で効果的
  • ローカル状態を管理する機能を提供していない
  • 拡張機能(Exchange)の仕組みがわかりやすい
  • Next.js サポートプラグインの提供

個人的には、この記事に書かれている Apollo Clientに対する辛みを感じきれていないところはありますが、urqlの推しポイントがわかったのが良かったです。
さわってみた感じ、urqlのドキュメントは確かにシンプルでわかりやすい印象でした。ベースとなるコア機能をシンプルに提供して、必要に応じて機能拡張する方法を公式から出してるのは良さそうだと思いました。

比較

apollo client、relay、urql のGraphql Client ついて、それぞれの比較がまとめられてるhasuraの記事がわかりやすかったです。

https://hasura.io/blog/exploring-graphql-clients-apollo-client-vs-relay-vs-urql/

おわりに

  • ドキュメントキャッシュがシンプルなキャッシュの仕組みになっているので、複雑なキャッシュ管理が不要の場合は採用すると管理が楽そうで良さそう
  • ドキュメントが読みやすかった
  • Exchangeの仕組みが便利で良かった。ベースとなるコア機能をシンプルに提供して、必要に応じて機能拡張していくという考え方すごくいいなと思った
  • ドキュメントの冒頭にあったように、highly customizable(高度にカスタマイズが可能)で versatile(汎用性が高い)な GraphQL Client だということがわかって勉強になった

参考

https://zenn.dev/murasaki/articles/fbf7efa744ffa3

https://user-first.ikyu.co.jp/entry/2022/07/01/121325

https://kitten.sh/graphql-normalized-caching

https://zenn.dev/takurinton/articles/ee14cdd8a1630c

https://youtu.be/EoM-1Lq0rjU

https://zenn.dev/tapioca/scraps/977454673f640d

GitHubで編集を提案
株式会社モニクル

Discussion