ApolloClient + Reactのキャッシュ操作について

#はじめに
フロントエンド側でGraphqlを操作するのに最近はApolloClient + Reactを使用していますが、実装してて思うのが、

***・キャッシュ操作が難しい***
***・日本語情報が全然ない***

ということです。
そこで今回は、ApolloClient + Reactのキャッシュ操作に重点を置いて説明しようと思います。

#1.ApolloClientのキャッシュの仕組み

ApolloClientはGraphqlでサーバーサイドから取得したクエリをインメモリのキャッシュ(InMemoryCache)に正規化し保存する機能があります。
ApolloClientがデータを取得する時はデフォルトの設定だとまずキャッシュを参照し、
**・データがキャッシュにあればキャッシュからデータを取得**
　　　　　　　　　　　　↓
**・なければサーバーサイドからクエリを取得し、キャッシュする**
という仕組みです。Graphqlのクエリだけではなく、認証情報などの状態もキャッシュしておくことができます。

フェッチポリシーやキャッシュデータの正規化に関しては公式ブログの方が詳しく画像付きでわかりやすく説明されているのでこちらを参照することをお勧めします。
[フェッチポリシーについて](https://medium.com/@galen.corey/understanding-apollo-fetch-policies-705b5ad71980)
[ApolloCLientのキャッシュの正規化のしくみ](https://www.apollographql.com/blog/demystifying-cache-normalization/)

#2.ApolloClientのキャッシュ操作

では「キャッシュを操作する必要のある状況」はいったいどういう時でしょうか？
それは**「データベースに格納されている値が追加(insert)、変更(update)、削除(delete)された時」**や**ログイン等でローカルの状態が変更された時**がほとんどだと思います。
Graphqlのミューテーションでデータベースに格納されている値が追加、変更、削除された時やログインでローカルの状態が変化した時などは、何も処理をしなければ**当然ApolloCLientのキャッシュの内容は変更されません。**そのためApolloClientで適切なキャッシュ操作を行い、キャッシュを更新する必要があります。
ApolloClientはキャッシュ操作のための様々なメソッドが用意されており、これらを使いキャッシュを操作していく必要があります。

###前提条件
```graphql
query GET_TODOS {
    id
    text
}

mutation ADD_TODO(text: $text) {
    id
    text
}

```

今回例ではTodoリストの全件を取得する「getTodos」と新たにTodoリストに追加する「addTodos」を使用します。

```javascript
import { useQuery, useMutation } from '@apollo/client';

const { data } = useQuery(GET_TODOS, {
    //ここにキャッシュ操作のメソッドを記述する
});

const [ addTodo ] = useMutation(ADD_TODO, {
    //ここにキャッシュ操作のメソッドを記述する
})
```

また今回はApolloClientが提供しているReact用のフックを使用してクエリ、ミューテーションを実行した後、キャッシュ操作のメソッドを記述していくものとします。

```javascript
TODO:1 {id: 1, __typename:'TODO', text: 'テスト'},
TODO:2 {id: 2, __typename:'TODO', text: 'テスト'}
```
またApolloClientのキャッシュにはこのような形でデータが保存されていることとします。


###操作方法１・refetchQueries
１つ目のキャッシュ操作方法は「refetchQueries」です。
このメソッドは指定したクエリをキャッシュではなくGraphQLサーバーにリクエストを送り、データを取得及びキャッシュします。

```javascript
const [addTodo] = useMutation(ADD_TODO, {
    refetchQueries: [{query: GET_TODOS}]
})
```
この例では、「ADD_TODO」ミューテーションでTODOリストにデータを追加した後、サーバーに「GET_TODOS」クエリのリクエストを送っています。

####特徴
・コードの記述量が少なく済みます。後述するread/writeQuery、read/writeFragmentよりもコードの記述量が圧倒的に少ないです。
・すでにキャッシュされたデータを考慮せずに取得するため、データを余計に取得してしまう可能性があります。
例えばすでにTODOリストのデータがすでに30件キャッシュされていたとしても、refetchQueriesはそれを考慮せずもう一度サーバーからデータを取得するため、30件分余計にデータを取得してしまうことになります。

###操作方法２・read/writeQuery
２つ目のキャッシュ操作方法は「read/writeQuery」です。
**readQuery**はApolloClientに保存されているキャッシュを返し、**writeQuery**はミューテーションの結果等を正規化した状態でキャッシュに書き込むことのできるメソッドです。
ApolloClient+hooksで使用する場合は`update`関数を用いて記述していきます。

```javascript
const [addTodo] = useMutation(ADD_TODO, {
    //引数「cache」はApolloCLientのキャッシュ操作ができる引数
    //引数「result」はミューテーションの返り値が格納されている
    update(cache, result) => {
        const existCache = cache.readQuery({
            query: GET_TODOS,
        })
        if (existCache) {
            cache.writeQuery({
                query: GET_TODOS,
                data: {
                    getTodos: {
                        ...existCache,
                        result
                    }
                }
            })
        }
    }
})

```

例は保存されているTODOリストのキャッシュを`cache.readQuery`で呼び出し、キャッシュが存在したなら(`readQuery`はキャッシュが存在しない時は`null`を返します)`cache.writeQuery`で既存のキャッシュにミューテーションの返り値である`result`を既存のキャッシュに加えて保存しています。

####特徴
・すべてキャッシュを使用して行うため、refetchQueriesを使用するよりも高速です。
・refetchQueriesに比べるとコードの記述量は多くなります。

###操作方法３・read/writeFragment
３つ目のキャッシュ操作方法は「read/writeFragment」です。
「Fragment」と聞くとGraphqlを使っている方なら「フラグメントの更新にしか使用できないの？」と思ってしまうかもしれませんが、そういうわけではなく、read/writeFragmentを使用することで**キャッシュの部分的な読み込み/書き込みが可能になります**。読み込み、書き込みの際もキャッシュに保存されている形式に囚われずに読み込み/書き込みが可能になります。

```javascript
const [addTodo] = useMutation(ADD_TODO, {
    update(cache) => {
        const existCache = cache.readFragment({
            id: "TODO:2"//readFragmentを使用する場合、読み込むキャッシュのID指定は必須
                fragment: gql`
                    fragment _ on TODO {
                        text
                    }
                `
        })
        if (existCache) {
            cache.writeFragment({
                id: "TODO:2"//writeFragmentを使用する場合、書き込むキャッシュのID指定は必須
                fragment: gql`
                    fragment _ on TODO {
                        text
                    }
                `,
                data: {
                    text: "変更"
                }
            })
        }
    }

})
```

例ではキャッシュの「TODO:2」をreadFragmentで読み込み、キャッシュがあればwriteFragmentでキャッシュの`text`のみを変更しています。

####特徴
・ミューテーションの返り値がGraphqlのクエリの形式ではない場合(booleanとか)でも使用することができます。(read/writeQueryの場合、キャッシュから読み込む又は書き込むクエリはキャッシュに保存されている形式である必要が有ります)


#３.まとめ

今回はApolloClient+Reactでのキャッシュ操作について紹介しました。
今後もApolloClient、ApolloServerの記事をどんどん書きたいと思っていますので、よろしくお願いします！