GraphQL におけるトランザクション処理は Mutation と呼ばれます。トランザクション処理の結果に基づいてビューも更新するのが一般的ですが、GraphQL の場合、いくつかの手法があります。それらの手法、どれを採用すべきかについては、後続の記事にて解説するとして、今回は前提知識となる キャッシュ機構のふるまいと利用方法について書きます。なお、この記事は React Apollo(v2)の仕様に基づいています。
Apollo のキャッシュ機構
Apollo ではキャッシュ機構として InMemoryCache が提供されています。Apollo client に組み込むことで、redux の Store のようにキャッシュを管理することができます。
import { InMemoryCache } from 'apollo-cache-inmemory'; import { HttpLink } from 'apollo-link-http'; import { ApolloClient } from 'apollo-client'; const cache = new InMemoryCache(); const client = new ApolloClient({ link: new HttpLink(), cache });
キャッシュの仕組み、データの正規化
InMemoryCache は、リクエストされたクエリを保存する際に、データの正規化を行います。具体的にはデータオブジェクトに __typename
と _id
ないしは id
を設定することで、各データオブジェクトをユニークに扱えるようにします。例をみてみます。以下のような Query に対して、クエリレスポンスのデータオブジェクトにクエリの結果以外に__typename
がついているとします。*1
// Query place(id: $id) { address id lat lng name } } } // クエリのレスポンス { address: "63 Ellis St" id: "12345" lat: 37.785495 lng: -122.407103 name: "John's Grill" __typename: "Place" }
この場合、cache には以下のように保存されます。
Place:12345:{...}
もし、__typename
やid
が設定されていない場合はどうなるでしょうか?その場合はROOT_QUERY.{クエリ名}
に順にキャッシュされるようになります。
ROOT_QUERY.place({ id: 49c7dba5f964a520c8571fe3 }):{...}
この場合、クエリが増えると、後述のreadQuery
などでキャッシュから読み込む際に、特定の型のオブジェクトを分離して取得するのが難しくなりますし、Apollo-link などで cache をログする場合に可読性が下がるので、__typename
とid
は必ず設定するべきです。
ちなみに、このユニーク id の設定のロジックはapollo-cache-inmemoryパッケージのこの辺に書いてあります。
キャッシュのユニークなデータアクセスは__typename:id
をキーとすることで可能になります。しかしこれだと__typename
が何なのか、実装者が把握している必要があるし、そもそも Query や Fragment ごとのid
がユニークであれば__typename
は冗長です。そこで InMemoryCache では key を dataIdFromObject
で指定することができるようになっています。
const cache = new InMemoryCache({ dataIdFromObject: object => object.id });
これで id のみを指定してデータへアクセスが可能となりました。
キャッシュへのアクセス
さて、キャッシュへのアクセス方法は大きく分けて 3 つあります。
(1) ApolloConsumer からダイレクトにアクセスする方法
ApolloConsumer は React の Context Consumer のように Apollo client をサブスクライブして受け取ることが可能です。 受けた Apollo client からキャッシュへ書き込みができます。ただし提供されている writeData
メソッドは新たにデータを書き込むなどの単純なキャッシュ更新がしかできないので、あまり使い勝手はよくありません。
// root.js <ApolloProvider client={client}> <MyRootComponent /> </ApolloProvider> ... // FilterLink.js const FilterLink = ({ filter, children }) => ( <ApolloConsumer> {client => ( <Link onClick={() => client.writeData({ data: { visibilityFilter: filter } })} > {children} </Link> )} </ApolloConsumer> );
参考: https://www.apollographql.com/docs/react/api/react-apollo/#apolloconsumer
(2) resolver 経由でアクセスする方法
より複雑なキャッシュ更新、例えばリストの更新やオブジェクトの書き換えが発生する場合は、resolver を使うことが推奨されています。 resolver はまさにローカルの状態管理を行うための仕組みです。
// resolverの定義 const resolvers = { Mutation: { toggleTodo: (_root, variables, { cache }) => { const id = variables.id const fragment = gql` fragment completeTodo on TodoItem { completed } `; const todo = cache.readFragment({ fragment, id }); const data = { ...todo, completed: !todo.completed }; cache.writeFragment({ id, fragment, data }); return null; }, }, } // clientの初期化(resloversの設定) const client = new ApolloClient({ cache: new InMemoryCache(), resolvers, });
mutation に紐付いた resolver を呼び出す際は、GQL に対して、@client
ディレクティブを設定します。
const TOGGLE_TODO = gql` mutation ToggleTodo($id: Int!) { toggleTodo(id: $id) @client // <- resolverを見に行く } `;
キャッシュを読み書きする方法も2通りあります。まずは readQuery
/writeQuery
で、まんま Query を読み書きできます。細かい話なのですが、キャッシュがない場合、readQuery は null ではなく error を throw するんですよね。そのため try/catch で挟むことをおすすめします。あまり良いデザインではないので、この件については何度もissue に上がっているのですが、未だに進捗がないようです。
もう一つは readFragment
/writeFragment
です。これは対応する id があれば、クエリに依存することなくキャッシュを読み込むことができます。どういう用途で使われるかというと、例えば色々な画面で呼び出されるデータ(アイテム名など)を一度に更新することができます。便利ですね。もちろんこれを使う場合はクエリを Fragment として分割しておく必要があります。
(3) Mutation コンポーネント経由でアクセスする方法
さて、最後に紹介する方法は React Apollo が提供する Mutation コンポーネントを利用したパターンです。
Mutation には update というオプションがあります。これはまさに Mutation の返り値を受けて、キャッシュを更新するためにあります。
以下の例では旅を予約する Mutation の結果を受けて、旅一覧(trips)にデータを追加しキャッシュを更新するということをやっています。
export default function BookTrips({ cartItems }) { return ( <Mutation mutation={BOOK_TRIPS} variables={{ launchIds: cartItems }} update={(cache, result) => { const trips = cache.readQuery(TRIPS); const newTrip = Object.assign(trips, result.trip); cache.writeQuery(newTrip); }} > {(bookTrips, { data, loading, error }) => (...)} </Mutation> );
なお、 variable
やupdate
は props
ではなく以下のように mutation 関数の引数として渡すことも可能です。
export default function BookTrips({ cartItems }) { return ( <Mutation mutation={BOOK_TRIPS} > {(bookTrips, { data, loading, error }) => ( bookTrips({ variables: { launchIds: cartItems }, update:{(cache, result) => {...}}) )} </Mutation> );
update
利用時の注意点は、副作用のあるロジックは組み込んではいけないことがあります。なぜなら update
は複数回呼ばれるケースがあるからです。具体的には Optimistic Response なのですが、これは次の記事について触れたいと思います。
ちなみに、ただキャッシュ上のオブジェクトを上書きしたい場合は、update
は必要ありません。Mutation のレスポンスとして更新したいデータの id と更新後の値を返すことで、キャッシュデータは自動的に更新されます。
const GET_TODOS = gql` query GetTodos { todos } `; const UPDATE_TODO = gql` mutation UpdateTodo($id: String!, $type: String!) { updateTodo(id: $id, type: $type) { id type } } `; const Todos = () => ( <Query query={GET_TODOS}> {({ data }) => { return data.todos.map(({ id, type }) => { let input; return ( <Mutation mutation={UPDATE_TODO} key={id}> {updateTodo => ( <div> <p>{type}</p> <form onSubmit={e => { e.preventDefault(); // トランザクション処理が成功すると、キャッシュ上のidを参照して更新される(Queryのdataが更新される) updateTodo({ variables: { id, type: input.value } }); input.value = ""; }} > <input ref={node => { input = node; }} /> <button type="submit">Update Todo</button> </form> </div> )} </Mutation> ); }); }} </Query> );
今日はここまで。次回は Mutation 後の更新処理についての指針について触れていきます。
参考ドキュメント:
https://www.apollographql.com/docs/react/essentials/local-state/
https://www.apollographql.com/docs/react/advanced/caching/