Apollo
- GraphqlのJavaScriptクライアント https://www.apollographql.com/docs/react/data/queries/
- この記事はReactでの使い方メモです
- 順番的にはGraphQLのドキュメントが先な気がします。 https://graphql.org/learn/
gql
- graphqlはJavaScriptっぽいけど全く別のクエリ言語
- gqlというgraphqlのパーサーでパースしたものを使用する
const GET_DOGS = gql`
{
dogs {
id
breed
}
}
`;
useQuery
- loading, error, dataが変わったら再レンダリングを行うReact Hook
-
const { loading, error, data, refetch, networkStatus } = useQuery(GET_DOGS)
こんな感じで使う - 第二引数にオプションを渡す。variablesもその1つ
useQuery(GET_DOGS, { variables: { json: "aaa" } })
- キャッシュは自動で有効になっていて、クエリA => クエリB => クエリAをすると、2回目は高速に表示される。サーバーと同期を取りたい時はpollingとrefetchingを行う必要がある
- Apollo内ではgraphQLの
__typename
とid
で同一のオブジェクトかどうか判定している
Polling
- 初回ロードした後キャッシュを使用し続けるので、最新の情報をとってくる必要がある。方法はPollingとRefetchingがある
- 一定の間隔でクエリを投げ続ける
- pollIntervalオプションで頻度を設定できる
const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
variables: { breed },
pollInterval: 500, 0.5秒ごとにクエリを発行する
});
-
startPolling
stopPolling
funcで、動的に開始したり終了したりできる。useQueryの戻り値の一つ
Refetching
- なんらかのイベントでクエリを再度投げる
const { loading, error, data, refetch } = useQuery(GET_DOG_PHOTO, {
variables: { breed }
});
<button onClick={() => refetch()}>Refetch!</button>
- variablesを与えることもできる。与えなければ、前回と同じvariablesが使われる
- もし一部のvariablesだけrefetchで更新した場合は、残りのvariablesは初回と同じものが使われる
<button onClick={() => refetch({
breed: 'dalmatian'
})}>Refetch!</button>
- refetchを呼んだ時、デフォルトだとloadingはずっとfalseのまま。変わらないのでレンダリングもされない。
-
notifyOnNetworkStatusChange: true
を渡すと、refetch中にloadingがfalseにすることができる。 - ただ、これだと初回ロード中なのかrefetchロード中なのかわからないので、
networkStatus
で判定する
const { loading, error, data, refetch, networkStatus } = useQuery(
if (networkStatus === NetworkStatus.refetch) return 'Refetching!';
if (loading) return null; ローディングは下に書かないとreturnしてしまう
- networkStatusは、8つの状況を返す。https://github.com/apollographql/apollo-client/blob/main/src/core/networkStatus.ts
Inspecting error states
- エラーの時にどういう挙動をするかを、
errorPolicy
オプションで指定できる。 - デフォルトは
none
で、エラーが起きたらレスポンスを破棄して何もせず、戻り値のerror
プロパティをtruethy
にするだけ。エラークラスが配列で入ってくる -
all
にすると、レスポンスを破棄せずそのレスポンスを返す - 他の処理方法についてはこれだけでセクションになっている Handling operation errors - Client (React) - Apollo GraphQL Docs
useLazyQuery
- useQueryはそのコンポーネントがレンダリングされた時に自動で実行されるが、
useLazyQuery
はなんらかのイベントでクエリを実行する const [getDog, { loading, error, data }] = useLazyQuery(GET_DOG_PHOTO);
- ボタン押したとか任意のタイミングで
getDog({options})
を呼ぶとクエリが実行される
fetch policy
-
useQuery
を呼んだ時に、まずは利用可能なキャッシュが無いか確認し、なければクエリを実行する。この挙動はfetch policyで設定できる。
- デフォルトは
cache-first
。その名の通り。 - 欲しいデータのうち1つでも足りないのがあればクエリを叩く
-
cache-and-network
ポリシーでは、キャッシュとクエリ両方からデータを要求する。まずはキャッシュを返しておき、後から取得したクエリの結果によってキャッシュが更新されたら、useQueryの結果自体も更新する。キャッシュ優先だけどもしサーバーのデータと異なってたらちゃんと更新しておきたい時に便利。
const { loading, error, data } = useQuery(GET_DOGS, {
fetchPolicy: "network-only" // キャッシュを確認せず毎回クエリを実行
});
-
fetchPolicy
とnextFetchPolicy
両方設定することで、1回目はfetchPolicy
の設定に従い、2回目以降はnextFetchPolicy
に従う
useMutation
- データを更新するmutatoinクエリを投げるために使う
import { gql, useMutation } from '@apollo/client';
const INCREMENT_COUNTER = gql`
mutation IncrementCounter {
currentValue
}
`;
function MyComponent() {
const [mutateFunction, { data, loading, error }] = useMutation(INCREMENT_COUNTER);
}
- mutationFunctionを任意のタイミングで呼べば、mutationが実行される
- useQueryと違い、レンダリング時には自動でクエリが実行されない
- data, loading, errorなどはuseQueryと似ている
- variablesの渡し方2つ
-
useMutation
の第二引数に渡すuseMutation(QUERY, { variables: { text: "hoge" } })
これはデフォルトのvariablesとして振る舞う -
mutationFunction
の引数に渡す。mutaionFunction({ variables: { text: "hoge" } })
。デフォルトを上書きする
-
- mutationした後、ローカルのキャッシュも更新するためにrefetchしたい。mutationのレスポンスでキャッシュを更新することは可能だが、複雑になりがちなので最初は単にrefetchを実行するのがおすすめとのこと。
Refetching Queries with Mutation
- useMutationの
refetchQueris
オプションに配列でgqlによってパースされたクエリか、クエリ名を与えると実行される - variablesは直近実行された時のvariablesがそのまま入る
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
refetchQueries: [
GET_POST, // DocumentNode object parsed with gql
'GetComments' // Query name
],
});
Updating the cache directly
- mutationのクエリの結果には大抵update後のオブジェクトが入っている。
- オブジェクトの情報が十分であれば、キャッシュが更新される。例えばUserの情報を更新後、Userの情報を取得したいみたいな時。
- しかし、mutationのクエリの戻り値に十分な情報がない、または関連する別のオブジェクトのキャッシュも更新しないといけない時には
update function
を使う必要がある https://www.apollographql.com/docs/react/data/mutations/#the-update-function
キャッシュについて深堀
- QueryかMutationが実行されるたびに、Apollo内部で正規化(normalization)が行われている。そのプロセスは、分解 => ID生成 => 保存(マージ + 上書き)である。
- レスポンスを解析し、id と __typenameを繋げ、データを一意に特定できるIDを作成する。例:
TodoType:1
- もしフィールドにidが存在しない場合には、データを特定できる他のフィールドをKey fieldsで指定する必要がある。
- このIDによって扱うデータを特定し、キャッシュしたり上書きしたりする
- レスポンスがネストしていたり配列だったとしても、TypeとIDによって一意に区別できる単位に分解し、フラットにした形でキャッシュする
- また、実行したクエリ自体の内容と、その時の変数、結果もキャッシュしているため、配列で取得した結果の順番も保持している。クエリの保存の際には、実データではなくType + IDで特定できる個々のデータへの参照だけを保持している
- キャッシュはJSONにシリアライズ可能。
cache.extract()
でその時点のキャッシュを取り出し、cache.restore(snapshot)
で再キャッシュすることもできる - キャッシュを更新する際、マージしながら上書きする。新しいフィールドが存在すれば追加し、重複していたら最新のデータに更新する
自動でキャッシュが更新されるケース
- Query。単にデータを取得する
- 1つのType+IDのアイテムをMutationで更新し、レスポンスとしてそのアイテムの新しいデータが返ってくる場合
- 複数のアイテムをMutationで更新し、レスポンスとして更新されたアイテムの配列を受け取る場合。
自動でキャッシュが更新されないケース
- これらはupdate functionで手動でキャッシュをmodifyするか、再度Queryを実行することでキャッシュを更新する必要がある。
- レスポンスのデータと関係のないキャッシュを更新したいケース(あたりまえ)。副作用(side-effects)を想定していて、例えばログアウトのMutationをした時に、ユーザーに紐づくキャッシュを消したい(更新したい)ケース。ログアウト自体のレスポンスは、
success
みたいな感じだからキャッシュのデータと関係ない - 複数のアイテムをMutationで更新するときに、レスポンスのフィールドが欠けている場合
- アイテムを生成する場合。例えばユーザー生成時に、ユーザー一覧に追加したいが、Apolloは新Itemを一覧に追加できるのか判定できないため。
- アイテムの削除。アイテムは消えても、既存の一覧などから消していいかどうかはApolloは判断がつかないため。
FieldPolicy
- Root Query Field Policy
- サーバーのgraphqlスキーマに存在しないクエリを投げると、キャッシュ等から任意のデータを取り出して返すことができる
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
readManagers: { // readManagersというクエリを作成してしまえる
read(managers, { readField }) {
const employees = readField('employees');
// Output:
// {
// __typename: 'EmployeesResponse',
// data: [
// { __ref: 'Employee:1' },
// { __ref: 'Employee:2' },
// ]
// }
return (employees?.data ?? []).filter(employeeRef => {
const employeeRole = readField('role', employeeRef);
return employeeRole === 'Manager'; // roleがManagerなemployeeを配列で返す
});
}
}
},
},
},
});