Closed15

urql のドキュメントを読むぜ

タピオカタピオカ

Overview

Overview | urql Documentation

urql はカスタマイズ性の高い GraphQL クライアント。
コアパッケージである @urql/core と、各種フレームワーク向けに core をラップした uqrl (React 向け), @urql/preact, @urql/svelte, @urql/vue などのパッケージから構成される。
Exchanges と呼ばれるアドオンパッケージを追加して機能拡張できる。

タピオカタピオカ

React/Preact Bindings

React/Preact Bindings | urql Documentation

セットアップ

createClient メソッドでクライアントを作成し、Context API で配下のコンポーネントに Client を提供するようにしておく。
後はコンポーネントで useQueryuseMutation メソッドでリクエストする。

const Todos = ({ from, limit }) => {
  const [result, reexecuteQuery] = useQuery({
    query: TodosListQuery,
    variables: { from, limit },
  });

  // ...
};

Variables が変わると新しいリクエストが送信される。

useQuery の一時停止

特定の条件を満たすまで Query の実行を一時停止できる。

const Todos = ({ from, limit }) => {
  const [result, reexecuteQuery] = useQuery({
    query: TodosListQuery,
    variables: { from, limit },
    pause: !from || !limit,
  });

  // ...
};

Mutations

const UpdateTodo = `
  mutation ($id: ID!, $title: String!) {
    updateTodo (id: $id, title: $title) {
      id
      title
    }
  }
`;

const Todo = ({ id, title }) => {
  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
};

useMutation は自動実行されない。
Mutation の結果を使うには、updateTodoResult を使う方法と updateTodo で返される Promise を使う方法の二通りある。
前者は Mutation の状態を UI で表示する際に、後者は Mutation が完了した後に副作用を追加する際に有用。

タピオカタピオカ

Document Caching

Document Caching | urql Documentation

urql のデフォルトのキャッシュ方法で、cacheExchange で実装されている。
Query と Variables の組み合わせをキーとしてキャッシュする方法。
キャッシュの TTL は無期限。
Mutation を送ると、その Mutation の対象と同じ __typename を含む Query のキャッシュは破棄される。

リクエストポリシー

いくつかのポリシーを選択できる。

Policy Description
cache-first キャッシュがある場合はキャッシュを、ない場合はリクエストを送る。
cache-and-network キャッシュを返し、リクエストも送る。
network-only キャッシュを無視し、常にリクエストを送る。
cache-only 常にキャッシュを利用する。キャッシュがない場合は null を返す。

Document Cache の注意点

データのリストをリクエストして空のリストが返ってきた場合、__typename が含まれないためキャッシュを破棄できない。
これに対処するには、Query の Context に additionalTypenames を追加するか、Normalized Caching を使用するかの2つの方法がある。

typename を明示する

以下のように Query に含まれる typename を明示しておくことで、リストが空の場合でも urql がキャッシュを破棄するタイミングを検知できる。

const context = useMemo(() => ({ additionalTypenames: ['Todo'] }), []);
const [result] = useQuery({ query, context });

Mutation で直接関係していないデータのキャッシュを破棄したい場合にも使える。

const [result, execute] = useMutation(`mutation($name: String!) { createUser(name: $name) }`);

const onClick = () => {
  execute({ name: 'newName' }, { additionalTypenames: ['Wallet'] });
};
タピオカタピオカ

Errors

Errors | urql Documentation

エラーは Network Error と GraphQL Error の2種類に分けられる。
それらを抽象化する CombinedError クラスがあり、networkErrorgraphQLErrors の2つのプロパティのうちいずれかを持っている。

Property Description
networkError リクエストを止めたすべてのエラー
graphQLErrors GraphQL API から受け取った GraphQLError の配列

また GraphQL では成功したリクエストでも部分的に失敗した箇所のエラーを含む可能性がある。

タピオカタピオカ

UI Patterns

UI-Patterns | urql Documentation

以下の UI パターンが紹介されている。

Pattern Description
Infinite scrolling いわゆる無限スクロール
Prefetching data ページ遷移する前にデータを取得したいようなケース
Lazy query コンポーネントがマウントされても Query をすぐ開始したくないケース
Reacting to focus and stale time Exchanges を利用してデータの入出力を操作したいケース
タピオカタピオカ

Architecture

Architecture | urql Documentation

Request と Operation

Query や Mutation は内部では Operation として管理される。
これは GraphQLRequest を拡張したもので、リクエストに関わる情報が保持される。
Operation は Exchange を通して伝達される。

Exchange

Exchange は Redux における middleware のようなもので、すべての Operation と Result にアクセスできる。
複数の Exchange をチェインして Operation を処理し、ロジックを実行する。

Operation が Exchange に届くまで

  1. リクエストが要求されると Operation を作成する
  2. Operation は自身が Query, Mutation, Subscription のいずれかであるかを識別し、一意な key を持つ
  3. Operation は Exchange に送られ最終的に fetchExchange に辿り着く
  4. API に Operation を送信し、結果は OperationResult でラップされる
  5. OperationResult を key でフィルタリングし、callback を通じて Result ストリームを提供する

いろいろな Exchange

@urql/core はデフォルトの Exchange として以下が含まれる。

Exchange Description
dedupExchange Operation の重複排除
cacheExchange Document Caching
fetchExchange fetch を使用して Operation を API に送信する

他にもいろいろある。

Exchange Description
errorExchange エラー発生時に呼び出されるグローバルコールバックを使用する
ssrExchange サーバーサイドのレンダラーがクライアント側の rehydration の結果を収集する
retryExchange Operation をリトライする
multipartFetchExchange multipart のファイルアップロード機能を提供する
persistedFetchExchange Persisted Query のサポートを提供する
authExchange 認証フローを追加する
requestPolicyExchange cache-only, cache-first の Operation を一定時間後に自動的に cache-and-network にアップグレードする
refocusExchange 実行中の Query を追跡しウィンドウがフォーカスを取り戻したときにそれらを再レンダリングする
devtoolsExchange urql-devtools を使用する機能を提供する

Stream

urql を普通に使う分には Stream を知らなくてもいいが、Exchange を自分で書いたりする際には知っておく必要がある。
この辺はとりあえず深追いしないことにする。

タピオカタピオカ

Subscriptions

Subscriptions | urql Documentation

subscriptionExchange を追加することで Subscription を使用できる。
これはサーバー側からなんらかの通知を受け取りたいケースで使用し、内部的には WebSocket を用いていて実現されている。

以下は新しいメッセージを通知するサブスクリプションを購読する例。

import React from 'react';
import { useSubscription } from 'urql';

const newMessages = `
  subscription MessageSub {
    newMessages {
      id
      from
      text
    }
  }
`;

const handleSubscription = (messages = [], response) => {
  return [response.newMessages, ...messages];
};

const Messages = () => {
  const [res] = useSubscription({ query: newMessages }, handleSubscription);

  if (!res.data) {
    return <p>No new messages</p>;
  }

  return (
    <ul>
      {res.data.map(message => (
        <p key={message.id}>
          {message.from}: "{message.text}"
        </p>
      ))}
    </ul>
  );
};
タピオカタピオカ

Persisted Queries and Uploads

Persistence & Uploads | urql Documentation

Automatic Persisted Queries

GraphQL ではデータ要件の形を表現するためのクエリ言語を使用する性質上、大きな Query は帯域幅を圧迫しパフォーマンスボトルネックになり得る。
Automatic Persisted Queries はクエリ文字列の代わりに生成された ID またはハッシュをリクエストとして送信することでこれを解決する。

流れとしては以下のような感じ。

  1. クライアントはクエリをハッシュ化して送信する
  2. サーバーがハッシュを知っていればそのままリクエストを処理し、知らなければ PersistedQueryNotFound エラーでレスポンスを返す
  3. PersistedQueryNotFound エラーを受け取ったクライアントは変わりに完全なクエリ文字列とハッシュをセットで送信し、クエリをサーバーに登録する

さらにハッシュ化されたクエリを GET リクエストで送ることもでき、これにより CDN がキャッシュしやすくなる。

persistedFetchExchange を使用して実装できる。

File Uploads

multipartFetchExchange を使用してファイルのアップロードをサポートできる。
これは Mutation の Variables にひとつでも File があれば、application/json の代わりに multipart/form-data POST リクエストを送信する。

タピオカタピオカ

Debugging

Debugging | urql Documentation

Devtools

urql devtools を使うことで、内部で発生したデバッグイベントを検査したり、データを調べたり、Query を実行できたりする。

デバッグイベント

デバッグイベントは devtools や client.subscribeToDebugTarget() メソッドを使うことでリッスンできる。

タピオカタピオカ

Testing

Testing | urql Documentation

クライアントのモック

Query, Mutation, Subscription で呼ばれるメソッド executeQuery, executeMutation, executeSubscription のモック関数を含むオブジェクトを作成し、Provider でラップする。

モック関数は以下を返すことで状態や結果をコントロールする。

Return value Description
never 永久に fetching: true になる
fromValue 即座に指定した結果やエラーを返す
makeSubject レスポンスを強制的にプッシュする。 Subscription のテストに有用
import { mount } from 'enzyme';
import { Provider } from 'urql';
import { never } from 'wonka';
import { MyComponent } from './MyComponent';

it('renders', () => {
  const mockClient = {
    executeQuery: jest.fn(() => never),
    executeMutation: jest.fn(() => never),
    executeSubscription: jest.fn(() => never),
  };

  const wrapper = mount(
    <Provider value={mockClient}>
      <MyComponent />
    </Provider>
  );
});

複数の Query に別々のレスポンスを返す場合は executeQuery の引数の query で条件分岐できる。

import { fromValue } from 'wonka';

let mockClient;
beforeEach(() => {
  mockClient = () => {
    executeQuery: ({ query }) => {
      if (query === GET_USERS) {
        return fromValue(usersResponse);
      }

      if (query === GET_POSTS) {
        return fromValue(postsResponse);
      }
    };
  };
});

Subscription のテスト

Wonka の interval ユーティリティや makeSubject ユーティリティを使用して、新しいデータの到着を時系列でシミュレートすることができる。
この辺はとりあえず深追いしないことにする。

このスクラップは2022/07/18にクローズされました