3
4

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の調査

Posted at

はじめに

個人開発でApollo Clientを使っていて、キャッシュ周りがややこしかったためところどころドキュメントを読みました。
でなんとなくまとめました。

Querys

サーバーの最新の状態をクライアントでも揃えたい。

polling

一定時間ごとクエリを叩くことでサーバーの最新の状態を持ってくる

公式より引用

function DogPhoto({ breed }) {
  const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
    variables: { breed },
    pollInterval: 500,
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
  );
}

この例では0.5秒毎にクエリを叩いている。useQueryの第二引数のpollIntervalで設定できる。 [start, stop]Pollingによりクエリを叩いてほしい時間を細かく設定できる。

Refetching

リフェッチ関数を実行することで最新の状態を持ってくる

function DogPhoto({ breed }) {
  const { loading, error, data, refetch } = useQuery(GET_DOG_PHOTO, {
    variables: { breed }
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <div>
      <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
      <button onClick={() => refetch()}>Refetch!</button>
    </div>
  );
}

useQueryからrefetch関数をもらえるため、その関数を例えばボタンを押した時などに実行することでサーバーに最新の状態を取りに行ける。
ロード状態が複雑になりがち

上記二つのときのロード状態管理をどうするのか

useQueryにより最初のフェッチ => ロード中(ロード画面) => フェッチ完了 => ビュー更新(初期画面)
からのそこから refetch => ロード中(ここをどうするのか) => フェッチ完了 => ビュー更新

useQueryの二つ目の引数で notifyOnNetworkStatusChange を設定することで、 networkStatus によりロード状態を追うことができる。

import { NetworkStatus } from '@apollo/client';

function DogPhoto({ breed }) {
  const { loading, error, data, refetch, networkStatus } = useQuery(
    GET_DOG_PHOTO,
    {
      variables: { breed },
      notifyOnNetworkStatusChange: true,
    },
  );

  if (networkStatus === NetworkStatus.refetch) return 'Refetching!';
  if (loading) return null;
  if (error) return `Error! ${error}`;

  return (
    <div>
      <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
      <button onClick={() => refetch()}>Refetch!</button>
    </div>
  );
}

考えられるロード状態は NetworkStatus を参考にする。全部で8個用意されている。src

NetworkStatus
  loading = 1,

  setVariables = 2,

  fetchMore = 3,

  refetch = 4,

  poll = 6,

  ready = 7,

  error = 8,

クエリを叩くタイミングを変えたい

デフォルトではuseQueryはコンポーネントがマウントされるタイミングで一度実行される。これを回避して、別のタイミングでクエリを叩きたい場合がある。二つ方法がある。

一つは useLazyQuery を使う。このフックから返される関数を実行したいタイミングで実行する。

二つ目は、fetch policyを使う。apollo clientは初回に叩かれたクエリをキャッシュし、次回以降再びクエリが叩かれると、クエリで要求したデータがキャッシュにあるかチェックする。そしてキャッシュにデータがある場合は、サーバーまでデータを取りに行かず、そのキャッシュからデータが返される。この機能をキャッシュファーストという。(apollo clientではクライアント側にキャッシュが実装されているため、キャッシュヒットした場合はネットワークを使用しなくて済む。apollo clientを使う場合、このキャッシュが結構重要で、クライアントの状態管理にも使われる)。
デフォルトでキャッシュファーストが備わっているため、それを使わずにフェッチするタイミングなどを定義することもできる。これが fetch policyで、適用方法は簡単↓。

const { loading, error, data } = useQuery(GET_DOGS, {
  fetchPolicy: "network-only"
});

サポートされているポリシー一覧

Mutations

基本的な使い方
コンポーネントマウント時は、useMutationによりmutationは実行されない。
代わりにmutation関数がもらえるので、それを使って好きなタイミングで実行する。
useQueryと同様にvaliablesも指定できる。

import { gql, useMutation } from '@apollo/client';

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;


function AddTodo() {
  let input;
  const [addTodo, { data }] = useMutation(ADD_TODO);

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { type: input.value } });
          input.value = '';
        }}
      >
        <input
          ref={node => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

ミューテーション実行後にキャッシュを更新したい

mutationによってサーバー側で値を更新しても、デフォルトではクライアントのキャッシュは更新されない。(単一のエンティティに対しての変更は自動で更新される。次の項目で解説)
apollo client のキャッシュファーストにより、queryでキャッシュの値を参照している。 => mutationによりサーバー側の値は更新されたけど、クライアント側のキャッシュの値は更新されていないため、見た目も更新されないということが起きる。

そのため、mutation実行後にクライアント側のキャッシュを更新する必要がある。
キャッシュを無視してqueryのpollingやfetchPolicyでサーバーから直接データを引っ張ってくることもできるが、これだとパフォーマンスが悪くなる。

単一のエンティティの更新

エンティティという言葉をTodoListの各todo要素だと捉えて読むとわかりやすいかも。
単一のエンティティの更新 = TodoListのtodo要素一つを更新
この場合は自動でキャッシュが更新される。

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: String!, $type: String!) {
    updateTodo(id: $id, type: $type) {
      id
      type
    }
  }
`;

function Todos() {
  const { loading, error, data } = useQuery(GET_TODOS);
  const [updateTodo] = useMutation(UPDATE_TODO);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.todos.map(({ id, type }) => {
    let input;

    return (
      <div key={id}>
        <p>{type}</p>
        <form
          onSubmit={e => {
            e.preventDefault();
            updateTodo({ variables: { id, type: input.value } });
            input.value = '';
          }}
        >
          <input
            ref={node => {
              input = node;
            }}
          />
          <button type="submit">Update Todo</button>
        </form>
      </div>
    );
  });
}

apollo client はidによりエンティティをキャッシュしているため、mutationで例えばTodoListのid番目のtodo要素を変更すると、キャッシュ内のidもその変更と同じように更新される。TodoListのidとキャッシュ内のidは同じなため、同じ変更が加えられる。また、アプリのUIはキャッシュの値を参照しているため、キャッシュが更新されるとUIも更新される。

エンティティの追加・削除

この場合は自動でキャッシュは更新されない。
例えば、追加の場合はそもそもキャッシュ内に追加される要素がないため、単一のエンティティの更新の時と同じようにはいかない。
そのため、mutation実行後に自分でキャッシュを更新する作業が必要になる。
キャッシュの更新はupdate関数内で行う。

const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
    }
  }
`;

function AddTodo() {
  let input;
  const [addTodo] = useMutation(ADD_TODO, {
    update(cache, { data: { addTodo } }) {
      cache.modify({
        fields: {
          todos(existingTodos = []) {
            const newTodoRef = cache.writeFragment({
              data: addTodo,
              fragment: gql`
                fragment NewTodo on Todo {
                  id
                  type
                }
              `
            });
            return [...existingTodos, newTodoRef];
          }
        }
      });
    }
  });

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { type: input.value } });
          input.value = "";
        }}
      >
        <input
          ref={node => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

update関数から渡されるwriteFragment・modifyなどキャッシュの操作をする関数の使い方

参照・出典

3
4
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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?