Skip to content

Instantly share code, notes, and snippets.

@kenmori
Last active December 5, 2020 20:31
Show Gist options
  • Save kenmori/e36f9f4c6bb4b4e5d104ad43688b1ed6 to your computer and use it in GitHub Desktop.
Save kenmori/e36f9f4c6bb4b4e5d104ad43688b1ed6 to your computer and use it in GitHub Desktop.
Apollo-client のInMemoryCache(キャッシュ)と向きあう

Apollo-clientのInMemoryCache(キャッシュ)と向きあう

InMeoryCacheのオブジェクトの様子

addTypename: true
cacheKeyRoot: CacheKeyNode {children: Map(89), key: {…}}
config: {fragmentMatcher: HeuristicFragmentMatcher, dataIdFromObject: ƒ, addTypename: true}
data: DepTrackingCache {data: {…}, depend: ƒ}
maybeBroadcastWatch: ƒ optimistic()
optimistic: []
silenceBroadcast: true
storeReader: StoreReader {cacheKeyRoot: CacheKeyNode, keyMaker: QueryKeyMaker, executeStoreQuery: ƒ, executeSelectionSet: ƒ}
storeWriter: StoreWriter {}
typenameDocumentCache: Map(26) {{…} => {…}, {…} => {…}, {…} => {…}, {…} => {…}, {…} => {…}, …}
watches: Set(13) {{…}, {…}, {…}, {…}, {…},

・Storeする前にそれぞれのオブジェクト個々のオブジェクトに分割して、識別子をふって、フラット化されたデータ構造に格納する

・オブジェクトに__typenameが見つかった場合、一意の識別子としてidと_idを使う

・もしid_idが記述されていない、もしくは__typenameが記述されていない場合、フォールバックとしてrootのqueryとしてallPeopleから返されたオブジェクトはROOT_QUERY.allPeople.0のようにROOT_QUERYにパスがつくられる

このデフォルトの設定はInMemoryCacheのコンストラクタ上にdataIdFromObjectで設定できる

readQuery

・データがキャッシュにない場合Errorをthrowする ・queryは適切なキャッシュがない場合serverにリクエストをする場合があるがreadQeuryはapollo serverにrequestしない。キャッシュに対してリクエストする ・もしreadQueryで読みたいリクエストの全てのデータがキャッシュ上にない場合はErrorが返され、もし全てのデータがある場合はキャッシュが返されます。自分が知っているデータを読み込むようにしてください

readFragment

readQueryはrootのquery型からのリクエストを可能にするが、 readFragmentはどこのnodeからのリクエストも可能にする

const todo = client.readFragment({
  id: ..., // `id` is any id that could be returned by `dataIdFromObject`.
  fragment: gql`
    fragment myTodo on Todo {
      id
      text
      completed
    }
  `,
});

最初の引数の idは読み込みたいキャッシュのidで、dataIdFromObjectから返されたidでなくてはならない

const client = new ApolloClient({
  ...,
  dataIdFromObject: object => object.id,
});

このように設定すればidが振られる。 が多くの場合デフォルトの__typename。 idの場合はこのようにキャッシュに対してリクエストとする

const todo = client.readFragment({
  id: '5',
  fragment: gql`
    fragment myTodo on Todo {
      id
      text
      completed
    }
  `,
});

注:ほとんどの人はdataIdFromObjectのIDに__typenameを追加します。 これを行う場合は、フラグメントを読むときに__typenameを追加することを忘れないでください。 そのため、例えばあなたのIDはTodo_5であって5だけではないかもしれません。

・もし、cache上にidが振られてなかったら nullが返ってくる ・もし、cache上にidが存在してもtextフィールド(上記のリクエストにあるtextフィールド)が存在していない場合 エラーが返ってくる

いいところは idさえ指定すればキャッシュのどこからでもそれを読み込むことができること。 例えばそれで得たtodo({ todo(id: 5) { ... } }), からのそれかもしれないし、 リストの ({ todos { ... } }), からのそれかもしれないし (mutation { createTodo { ... } }). で得たそれかもしれない

writeQuery と writeFragment

キャッシュに対して変更できる、ただ忘れてはいけないのはserverに対しての変更ではないこと リロードするとwriteQueryとwirteFragmentで書き込んだキャッシュは消えてしまう

・シグネチャ(引数の型、引数の数、返り値の型)はreadQueryとreqdFragmentと同じ

completedフラグを更新したい場合はこう

client.writeFragment({
  id: '5',
  fragment: gql`
    fragment myTodo on Todo {
      completed
    }
  `,
  data: {
    completed: true,
  },
});

この値を観測しているsubscriberはこのアップデートするためにレンダーが走ります

もしfetchしたtodoListデータにtodoを追加したい場合

const query = gql`
  query MyTodoAppQuery {
    todos {
      id
      text
      completed
    }
  }
`;

const data = client.readQuery({ query });

const myNewTodo = {
  id: '6',
  text: 'Start using Apollo Client.',
  completed: false,
};

client.writeQuery({
  query,
  data: {
    todos: [...data.todos, myNewTodo],
  },
});

mutationからのapollo-clientキャッシュをqueryのupdate関数で更新

例えば、 こちらのようなTodoを作るmutationを実行して

mutation TodoCreateMutation($text: String!) {
  createTodo(text: $text) {
    id
    text
    completed
  }
}

こちらで

query TodoAppQuery {
  todos {
    id
    text
    completed
  }
}

キャッシュに対して更新する場合。 (mutationの終わりに、実際にクエリを送信せずにmutationが終了した後にもう一度TodoAppQueryを送信したように、クエリに新しいtodoを含めるようにします。)

// We assume that the GraphQL operations `TodoCreateMutation` and
// `TodoAppQuery` have already been defined using the `gql` tag.
GraphQLのオペレーションに `TodoCreateMutation``TodoAppQuery`をすでに持ち、`gql`tagを使って定義されているものとします

const text = 'Hello, world!';

client.mutate({
  mutation: TodoCreateMutation,
  variables: {
    text,
  },
  update: (proxy, { data: { createTodo } }) => {
    // Read the data from our cache for this query. //キャッシュからこのquery(TodoAppQuery)を得る
    const data = proxy.readQuery({ query: TodoAppQuery });

    // Add our todo from the mutation to the end. //todoにmutationから戻りをcacheで得たmutationする前のデータに付け加える
    data.todos.push(createTodo);

    // Write our data back to the cache. //キャッシュに対して、dataに置き換えて、書き込む
    proxy.writeQuery({ query: TodoAppQuery, data });
  },
});

・proxyオブジェクトは他の変更に邪魔されないトランザクションを提供し、最後まで書き込みを完結させる

optimisticResponse

optimisticResponseをオプションでしたいした場合2回呼び出される 1回目はmutationが行われた直後に、そのデータと共に、 2回目は1回目のそれが行われた後に実際に本物のデータが返ってくる (WIP)

optimisticResponse vs update。違い

彼らは違うことをします。 optimisticResponseはサーバーからの応答を予測します。 あなたがすでにcacheにあるノードを更新しようとしているなら、これを使う

update更新機能はあなたのstoreを完全にコントロールさせます。 たとえば、新しいノードを作成した場合は、それを関連クエリに追加する必要があります。 その新しい実体として、Apolloはそれをどうするべきか自動的には知りません。

他の2つの答えを拡大すると、区別しているのは、「更新中」のものがすでにキャッシュに存在するかどうかです。

もしあなたが既存のアイテムを更新しているなら、
例えば、
ToDoアイテムのタイトルを編集するなら、
あなたはoptimisticResponseを必要とするだけです。
どうして?
キャッシュにはノードが含まれているので、
新しいノードで何か新しいことが起こったことを伝えるだけでよく、
それはすぐにUIに反映されます。

optimisticResponseは、mutationからの「即時の」結果データを提供するだけです。

2つ目のケースは、
リストに新しいTodoアイテムを追加することです。
まず、
キャッシュは新しいアイテムが作成されたことを知る必要があります。
更新属性をMutationに提供するとすぐに、キャッシュの状態を制御します。

updateはrefetchQueriesの代わりに実行されます。
つまり、キャッシュ状態を管理していることになります。

データの階層全体を再フェッチするのではなく、
updateを使用すると、
キャッシュにアクセスして必要なノードだけを変更/追加することができます。
しかし、あなたはまだmutationが終了するのを待っています。 
optimisticResponseと一緒にupdateを提供すると、
即座に想定される応答を提供し、
それを個人用の更新機能に渡します。
これにより、即座にキャッシュが更新されます。

これら2つがシナリオ2でペアになっているのは、
サーバーの応答を完全に回避しているためです。
「即時」応答をしただけでは、Apolloはまだサーバーがキャッシュを更新するのを待つモードにあります。 
updateを使うと、
それをハイジャックすることができ、
クライアント側でもできます。

see: https://stackoverflow.com/questions/49186523/optimisticresponse-vs-update-in-apollo-client

・update関数は複数回呼び出される可能性があるため、副作用には適していません。 ・また、プロキシ上のメソッドを非同期に呼び出すことはできません。


WIP

以下は雑にまとめたやつ

型について

常に値が帰ってくることを保証している -> String!

特性

Qeuryは並列、mutationは順次、1つ目が終わったら2つめ

__typename

  • __typenameは帰ってくる型を知らせるためにある
  • cacheをupdateするときに使われるdefaultでついてくるもの
  • unionTypeで帰ってくるオブジェックトに対してとの型で返ってきたか区別するために必要

語彙

Introspection ・・・shemaの情報を教えてくれるもの__shemaのゆうなもの

Normalization ・・・chache storeにいれるまえにqury結果を変換すること

  • レスポンスを一意のものに区別分けて、フラットにしてstoreすること

normaraize時にid__typenameがないとRootにキャッシュがつくられる

[Episode!]は配列のEpisode型。nullでないので必ず配列が返ってくる

fileName: [String!] -> リスト自体はnullでいいが要素はStringじゃないとダメなことを示している

参考

GraphQL Concepts Visualized

How to update the Apollo Client’s cache after a mutation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment