この記事は 207 Advent Calendar 2022の記事です。
207株式会社でソフトウェアエンジニアをしている id:ryo-rm です。
207ではiOS / Android向けのアプリケーション "TODOCUサポーター" を提供しており、GraphQL / Apollo Clientを活用しています。
Apollo Clientにはスマートなキャッシュ管理機構がありますが、 "TODOCUサポーター" では Local-only fields
を活用しているので、こちらについて紹介します。
環境
"TODOCUサポーター" は React Native製ですが、Apollo Clientは Reactと共通の @apollo/client
を使っています。
apollo3-cache-persist
でキャッシュを永続化しており、永続化したデータの保存先は react-native-mmkv
を使っています。*1
@apollo/client
:^3.5.5
apollo3-cache-persist
:^0.14.1
@graphql-codegen/cli
:^2.3.0
@graphql-codegen/near-operation-file-preset
:^2.2.2
@graphql-codegen/typescript
:^2.4.1
@graphql-codegen/typescript-operations
:^2.2.1
@graphql-codegen/typescript-react-apollo
:^3.2.2
react-native-mmkv
:^2.4.3
Computed Fieldとして使う
バックエンドのレスポンスを加工して表示用に整形する、というのはよくあると思います。
"TODOCUサポーター" では、カラーコードに変換する処理などで使っています*2。
当アプリは配送員の方向けのアプリケーションであり、荷物それぞれに時間指定の概念が存在します。時間指定は 午前中/12時~14時/14時~16時などの配達時間帯がありますが、この時間は各社バラバラになっているので、 "時間指定枠" という概念を用意し、個別に変更できるようにしています。
データ構造としてはバックエンドに指定枠の番号 (1~9) が保存されており、フロントエンドで指定枠の番号に対して固定のカラーコードを発行する処理になっています。
データ構造を簡単に表わしてみると以下のようになっています。*3
type Product = { id: string; # 時間指定枠 timeframeIndex: number; };
今回は、 timeframeIndex
からComputed field のカラーコード timeframeColor
を作るのを書いてみます。
ドキュメントの定義
GraphQL Code Generator
を使っている場合、 local-schema.graphql
などのファイルを用意してドキュメントを生成することで既存の型を拡張することができます。
extend type Product { """ 時間指定枠の色 timeframeIndex とセットで利用する必要がある ローカル変数なので、 キャッシュを使わず取得した場合は存在しない """ timeframeColor: String }
ドキュメントに記載していますが、計算対象に使っている値がキャッシュに存在しない場合は計算できません。
すなわち、上記の timeframeColor
を取得するためには timeframeIndex
とセットでQueryに書いて取得しキャッシュに書き込まれる必要があります。
キャッシュを使っているので、Apollo Clientの FetchPolicies
で network-only
を使った場合は取得できません。
TypePolicyの定義
Apollo CacheのTypePolicyを事前に定義しておくことで、フィールドの値から計算した結果を返却することができます。
import type { TypePolicies, FieldPolicy } from '@apollo/client'; import { ApolloClient, InMemoryCache } from '@apollo/client'; export const typePolicies: TypePolicies = { Product: { fields: { timeframeColor: { read(exists, { readField }) { const timeframeIndex = readField<number>('timeframeIndex'); // ここで計算する switch (timeframeIndex) { case 1: return '#F06292'; default: return '#9E9E9E'; } }, } as FieldPolicy<string>, }, }, }; const cache = new InMemoryCache({ typePolicies, }); const client = new ApolloClient({ cache, });
readField
で Product.timeframeIndex
を参照しています。キャッシュが存在しない場合もあるので、そのケースについても考慮するようにしています。
クエリ
ここまで来たら、 @client
をつけて取得するだけです。
ドキュメントの定義
で書いたように、計算元の値がキャッシュに存在する必要があります。
query { product(id: "1") { id timeframeIndex timeframeColor @client } }
テストコード
jestでテストコードを書く場合は以下のように書くことができます!
import { gql, InMemoryCache } from '@apollo/client'; import { typePolicies, DEFAULT_COLOR_VALUE, } from '~/src/api/apollo/typePolicies/product'; describe('localVariable/product', () => { describe('timeframeColor', () => { test('時間指定枠 1 からカラーコードが返却される', () => { const cache = new InMemoryCache({ typePolicies, }); const colorQuery = gql` query { product(id: "1") { timeframeIndex timeframeColor } } `; cache.writeQuery({ query: colorQuery, data: { product: { __typename: 'Product', timeframeIndex: 1, }, }, }); const actual = cache.readQuery<{ product: { timeframeColor: string; }; }>({ query: colorQuery }); expect(actual?.product?.timeframeColor).toEqual('#FFF'); }); test('時間指定枠の番号がキャッシュに存在しないとき、デフォルト値が返却される', () => { const cache = new InMemoryCache({ typePolicies, }); const colorQuery = gql` query { product(id: "1") { timeframeColor } } `; cache.writeQuery({ query: colorQuery, data: { product: { __typename: 'Product', // timeframeIndex: 9, // キャッシュに値が存在しない }, }, }); const actual = cache.readQuery<{ product: { timeframeColor: string; }; }>({ query: colorQuery }); expect(actual?.product?.timeframeColor).toEqual(DEFAULT_COLOR_VALUE); }); }); });
P.S.
この記事は、 207 Advent Calendar 2022の記事でした。
記事の都合で今回は書けませんでしたが、Reactive variables を利用していたり、永続化したキャッシュを使ってアプリ立ち上げ時の初期表示高速化& cache-and-network
で最新化するなど、Apolloキャッシュを活用しています。
Apollo Clientのキャッシュ機構は複雑で難しいですが、使いこなすことができたら強力です!
この記事はReact Nativeでの利用について書いていますが、React Native特有の機能は利用していないため、Webアプリケーションでも同じように活用できると思います。
最後にいつもの宣伝で、
207株式会社では、レガシーな物流業界の変革に挑む配達員向け効率化アプリ「TODOCUサポーター」を開発しています。 GraphQLでガンガン開発していきたいメンバーを大絶賛募集中です!
もし少しでもご興味がありましたら以下のnotionをご覧ください!
*1:Webで言う、localStorageのようなもの
*2:配送員の現在地から配送先への距離にも使っています。配送員の現在地は Reactive variables に保存しています
*3:実際には時間指定枠で1つのデータモデルになっています