データフェッチをgraphqlにする場合、clientライブラリとしてapllo-graphqlを選ぶことは多いと思います。しかし便利な機能を多く抱え込みすぎており、特にキャッシュ戦略は複雑でtoo muchな印象があります。そこで、よりシンプルでかつカスタマイズも柔軟にできるurqlを紹介します。
urqlとは
urqlは、JS及びReactのコンサル企業であるFormidableによって開発された、新しいgrqphql clientです。apolloをご存知の方は、react-apollo相当と思ってもらえればよいです。メインメンテナはstyled-componentsのメンテナでもあるPhil Plückthun。
下記のような特徴をもっています。
- apolloより軽量なgraphql client
- apolloよりシンプルなキャッシュ戦略
- reduxやexpressのmiddlewareに似た"Exchange"による拡張性
公式ドキュメント: https://formidable.com/open-source/urql/docs/
streamを扱うライブラリであるwonka、及びそのラッパーであるreact-wonkaといった技術の上に作られています。よって、中身のコードはoperation(ネットワークリクエスト抽象)をstreamで扱うような構造になっており、そこにExchange(middlewareのようなもの)を注入することで拡張させることができるようになっています。
使い方
codesandboxで良いという人はこちら(国の一覧と詳細を持つ簡単なデモアプリです)
https://codesandbox.io/s/simple-urql-example-wz6zw
セットアップ
apolloよりも簡単にセットアップできます
import { Provider, createClient } from 'urql'
const client = createClient({
url: 'http://localhost:4000/graphql',
})
const YourApp = () => (
<Provider value={client}>
{/* ... */}
</Provider>
)
useQueryによる基本的なリクエスト
useQueryを使った基本的なリクエストの例です。option引数にパラメターを渡すことで振る舞いをカスタマイズすることができるようになっています。
import React from 'react'
import { useQuery } from 'urql'
const getTodos = `
query GetTodos($limit: Int!) {
todos(limit: $limit) {
id
text
isDone
}
}
`
const TodoList = ({ limit = 10 }) => {
const [result] = useQuery({
query: getTodos,
variables: { limit },
});
if (result.fetching) return 'Loading...'
if (result.error) return <div>{result.error.message}</div>
return (
<ul>
{result.data.todos.map(({ id, text }) => (
<li key={id}>{text}</li>
))}
</ul>
)
}
Refetching (再リクエスト)
再リクエストは、第引数で得られるexecuteQuery
で実現できます。
function() {
const [res, executeQuery] = useQuery({
query: getTodo,
variables: { id }
})
return (
<div>
<div>{res.data.todo}</div>
<button onClick={executeQuery}>refetch</button>
</div>
)
}
条件付きリクエスト
何らかの条件が揃うまでリクエストを送りたくない場合はpauseオプションを設定します。
例えばidが別の非同期処理の結果を待たなければいけないときに使えます。
const [result] = useQuery({
query: getTodo,
variables: { id },
pause: id
});
キャッシュ
デフォルトのキャッシュの仕組み
urqlは"document" cacheの仕組みを持っています。すべてのquery + variablesの組み合わせはハッシュ化されレスポンスとともにキャッシュされるので、再度同じquery + variablesでリクエストが送られると、urqlはキャッシュされた結果を返します。ただし、同じ__typename
のmutationが送られたときはリソースが更新されたとみなされ、キャッシュはクリアされる仕組みになっています。
キャッシュをコントロールする - requestPolicy
urqlのQueryのキャッシュは、requestPolicyを設定することで自在に操ることができます。デフォルトの挙動は、cache-first
(キャッシュがあればリクエストを送らずキャッシュデータを返す)です。
urqlのキャッシュ戦略(request policy)は4種類で、とてもシンプルです。例えばcache-and-network
は、キャッシュデータを返しながら、同時に最新をフェッチしに行く挙動をします。つまりユーザーはキャッシュがあればローディングを待たず即結果をみることができ、かつ少し遅れて最新のものを見ることができます。
useQuery({ query: q, requestPolicy: 'cache-and-network' });
4つのrequestPolicy
Policy | 内容 | 使い所 |
---|---|---|
cache-first | デフォルト。キャッシュがあれば必ずキャッシュを返す。 | 変更のすくないリソースへのクエリ |
cache-only | リクエストは投げず、常にキャッシュのみを返す | SSR等、トリッキーな用途。通常は使わない |
network-only | キャッシュを使わず、常にリクエストを投げる | 頻繁に変わるリソースへのクエリ |
cache-and-network | キャッシュがあればキャッシュを返し、その後リクエストを投げる | 頻繁には変わらず、早く見せたいリソースへのクエリ |
Exchange
urql特徴の一つが、Exchangeです。
urqlでは、リクエスト関数をoperationという単位で扱います。このoperation lifecycleは、リクエストが投げられてから返ってくるまでの「双方向」の流れをstream的に扱います。
例えば、リクエストとレスポンスを簡易的にコンソールに出力するようなExchangeはこのように書けます。
export const loggerExchange: Exchange = ({ forward }) => {
if (!isDev) {
return ops$ => forward(ops$)
} else {
return ops$ =>
pipe(
ops$,
tap(op => {
console.log(
'[Exchange debug]: Incoming operation: ',
op.query.definitions[0].name.value,
`variables: ${JSON.stringify(op.variables)}`
)
}),
forward,
tap(result => {
if (result.error) {
console.log(
`[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
result.error
)
} else {
console.log(
`[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
result.data
)
}
})
)
}
}
その他の便利な機能
dev tool
Exchangesではコンソールにログを出力するサンプルを書きましたが、公式でリッチなデバッグツールも作られています。
https://formidable.com/open-source/urql/docs/advanced/debugging/
Graphcache
urqlのデフォルトのキャッシュ戦略は紹介したとおりですが、Graphcacheを使うことによりApollo ClientのようなNormalized Cacheを適用することもできます。
まとめ
今回紹介したurqlの他に、react-queryやSWRなど、ちょっとした群雄割拠状態となっているfetcherライブラリ界隈ですが、urqlはその中でもかなり特徴的な設計思想をもったライブラリです。
Graphqlの場合はApollo clientを使うケースが多いと思いますが、やや多機能すぎる嫌いがあります。特にアプリケーションがシンプルな場合はApolloの代替手段として、よりシンプルなurqlを検討してみてはいかがでしょうか。