9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Apollo Client ドキュメントメモ

Last updated at Posted at 2021-09-21

Apollo

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の__typenameidで同一のオブジェクトかどうか判定している

Polling

  • 初回ロードした後キャッシュを使用し続けるので、最新の情報をとってくる必要がある。方法はPollingとRefetchingがある
  • 一定の間隔でクエリを投げ続ける
  • pollIntervalオプションで頻度を設定できる
const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
    variables: { breed },
    pollInterval: 500, 0.5秒ごとにクエリを発行する
  });
  • startPolling stopPollingfuncで、動的に開始したり終了したりできる。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してしまう

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" // キャッシュを確認せず毎回クエリを実行
});
  • fetchPolicynextFetchPolicy両方設定することで、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を配列で返す
            });
          }
        }
      },
    },
  },
});

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?