urql のドキュメントを読むぜ
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Overview
urql はカスタマイズ性の高い GraphQL クライアント。
コアパッケージである @urql/core と、各種フレームワーク向けに core をラップした uqrl (React 向け), @urql/preact, @urql/svelte, @urql/vue などのパッケージから構成される。
Exchanges と呼ばれるアドオンパッケージを追加して機能拡張できる。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
React/Preact Bindings
React/Preact Bindings | urql Documentation
セットアップ
createClient
メソッドでクライアントを作成し、Context API で配下のコンポーネントに Client を提供するようにしておく。
後はコンポーネントで useQuery
や useMutation
メソッドでリクエストする。
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 が完了した後に副作用を追加する際に有用。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
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'] });
};
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Errors
エラーは Network Error と GraphQL Error の2種類に分けられる。
それらを抽象化する CombinedError クラスがあり、networkError
と graphQLErrors
の2つのプロパティのうちいずれかを持っている。
Property | Description |
---|---|
networkError |
リクエストを止めたすべてのエラー |
graphQLErrors |
GraphQL API から受け取った GraphQLError の配列 |
また GraphQL では成功したリクエストでも部分的に失敗した箇所のエラーを含む可能性がある。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
UI Patterns
UI-Patterns | urql Documentation
以下の UI パターンが紹介されている。
Pattern | Description |
---|---|
Infinite scrolling | いわゆる無限スクロール |
Prefetching data | ページ遷移する前にデータを取得したいようなケース |
Lazy query | コンポーネントがマウントされても Query をすぐ開始したくないケース |
Reacting to focus and stale time | Exchanges を利用してデータの入出力を操作したいケース |
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Architecture
Architecture | urql Documentation
Request と Operation
Query や Mutation は内部では Operation として管理される。
これは GraphQLRequest
を拡張したもので、リクエストに関わる情報が保持される。
Operation は Exchange を通して伝達される。
Exchange
Exchange は Redux における middleware のようなもので、すべての Operation と Result にアクセスできる。
複数の Exchange をチェインして Operation を処理し、ロジックを実行する。
Operation が Exchange に届くまで
- リクエストが要求されると Operation を作成する
- Operation は自身が Query, Mutation, Subscription のいずれかであるかを識別し、一意な key を持つ
- Operation は Exchange に送られ最終的に
fetchExchange
に辿り着く - API に Operation を送信し、結果は OperationResult でラップされる
- 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 を自分で書いたりする際には知っておく必要がある。
この辺はとりあえず深追いしないことにする。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
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>
);
};
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Persisted Queries and Uploads
Persistence & Uploads | urql Documentation
Automatic Persisted Queries
GraphQL ではデータ要件の形を表現するためのクエリ言語を使用する性質上、大きな Query は帯域幅を圧迫しパフォーマンスボトルネックになり得る。
Automatic Persisted Queries はクエリ文字列の代わりに生成された ID またはハッシュをリクエストとして送信することでこれを解決する。
流れとしては以下のような感じ。
- クライアントはクエリをハッシュ化して送信する
- サーバーがハッシュを知っていればそのままリクエストを処理し、知らなければ PersistedQueryNotFound エラーでレスポンスを返す
- PersistedQueryNotFound エラーを受け取ったクライアントは変わりに完全なクエリ文字列とハッシュをセットで送信し、クエリをサーバーに登録する
さらにハッシュ化されたクエリを GET リクエストで送ることもでき、これにより CDN がキャッシュしやすくなる。
persistedFetchExchange
を使用して実装できる。
File Uploads
multipartFetchExchange
を使用してファイルのアップロードをサポートできる。
これは Mutation の Variables にひとつでも File
があれば、application/json の代わりに multipart/form-data POST リクエストを送信する。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Server-side Rendering
Server-side Rendering | urql Documentation
ssrExchange
を使用してサーバー側でデータフェッチできる。
この辺はとりあえず深追いしないことにする。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Debugging
Debugging | urql Documentation
Devtools
urql devtools を使うことで、内部で発生したデバッグイベントを検査したり、データを調べたり、Query を実行できたりする。
デバッグイベント
デバッグイベントは devtools や client.subscribeToDebugTarget()
メソッドを使うことでリッスンできる。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Retrying Operations
Retrying Operations | urql Documentation
retryExchange
を使用してネットワークエラーなどの際に Operation をリトライできる。
リトライ条件や間隔などを細かく制御できる。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Authentication
Authentication | urql Documentation
authExchange
を使用して典型的な JWT ベースの認証フローで認証を実装できる。
この辺はとりあえず深追いしないことにする。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Testing
クライアントのモック
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
ユーティリティを使用して、新しいデータの到着を時系列でシミュレートすることができる。
この辺はとりあえず深追いしないことにする。
![タピオカ](https://res.cloudinary.com/zenn/image/fetch/s--vFk8bm6i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/e1ae39d5d9.jpeg)
Automatically populating Mutations
Auto-populate Mutations | urql Documentation
populateExchange
を使用すると @populate
ディレクティブを使えるようになり、Mutation 後に更新されたデータを View に自動反映するのに便利らしい。