Apollo Clientapollo3 cache persistExpo bareExpo Router CachePersistorpersistCachepersistoruseCachePersistorApolloClientusePersistCachedApolloClient

【ReactNative】GraphQL Frontend 環境〜apollo-client + apollo3-cache-persist〜

apollo-server-nexus-02 Apollo Client
スポンサーリンク

開発環境

この記事の環境の下で行います。

一箇所にまとめて置きたいので下記のように src/apollo/hooks ディレクトリーを作成します。

apollo3-cache-persist を使うとcache > persist > client 順に実行することが大事で、エラー処理も含めると少し複雑になるので useXXXApolloClient というカスタムフックを作成し、環境を作っていきます。

// apollo ディレクトリー構造: 現段階では⬇︎のように作成

try🐶everything myproject$ tree src   
src
├── apollo
│   ├── cache.js
│   └── hooks
│       ├── index.ts
│       ├── useCachePersistorApolloClient.tsx
│       └── usePersistCachedApolloClient.tsx
...

ApolloClient を追加する

依存関係をインストールする

Apollo Client を使用するアプリには、次の 2 つのトップレベルの依存関係が必要です。

  • @apollo/client: この 1 つのパッケージには、Apollo クライアントのセットアップに必要な事実上すべてが含まれています。 これには、メモリ内キャッシュ、ローカル状態管理、エラー処理、および React ベースのビュー レイヤーが含まれます。
  • graphql: このパッケージは、GraphQL クエリを解析するためのロジックを提供します。
    次のコマンドを実行して、これらのパッケージの両方をインストールします。
try🐶everything myproject$ npm install @apollo/client graphql

ApolloClient を初期化する

依存関係を設定したら、ApolloClient インスタンスを初期化できるようになりました。

app/_layout.tsx に ApolloProvider をラップします。⬇︎(Line 3、8-11、14、19を追加)

// app/_layout.tsx
...
import {ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client';    // <-- 追加する
...

function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const client = new ApolloClient({     // <-- client は 最終的にはカスタムフックに移動される
    uri: 'https://flyby-router-demo.herokuapp.com/',
    cache: new InMemoryCache(),  
  });                                                      

  return (
    <ApolloProvider client={client}> 
      <PaperProvider
        theme={colorScheme === 'dark' ? MD3DarkTheme : _MD3LightTheme}>
        ...
      </PaperProvider>
    </ApolloProvider> 
  );
}

console.log(client) コマンドなどを実行し、エラーなくメッセージが表示されれば OK です。

persistor を設定する

依存関係をインストールする

try🐶everything myproject$ yarn add apollo3-cache-persist
try🐶everything myproject$ yarn add @react-native-async-storage/async-storage
try🐶everything myproject$ npx pod-install
try🐶everything myproject$ yarn ios && yarn android

※ persistCache & CachePersistor 用 ApolloPersistOptions 詳細

// ApolloPersistOptions: persistCache & CachePersistor

export interface ApolloPersistOptions<TSerialized, TSerialize extends boolean = true> {
    cache: ApolloCache<TSerialized>;
    storage: StorageType<PersistedData<TSerialized>, TSerialize>;
    trigger?: 'write' | 'background' | TriggerFunction | false;
    debounce?: number;
    key?: string;
    serialize?: TSerialize;
    maxSize?: number | false;
    persistenceMapper?: PersistenceMapperFunction;
    debug?: boolean;
}

persistCache

デフォルトの persistCacheApolloPersistOptions ( Apollo キャッシュと基盤となるストレージ プロバイダー) をpersistCache に渡すだけで開始できます。(例⬇︎)

※ デフォルトでは、Apollo キャッシュの内容はすぐに復元され (非同期)、キャッシュに書き込むたびに (短いデバウンス間隔で) 保持されます。( React Native ではデフォルトでアプリが background 状態に変わると保持される)

cache は実際のプロジェクトでは詳細な typePolicies 設定を行うことがあるので別のファイルに分離しています。(任意)

try🐶everything myproject$ mkdir -p src/apollo/hooks
try🐶everything myproject$ touch src/apollo/hooks/useApolloClient.tsx
// src/apollo/cache.js

import {InMemoryCache} from '@apollo/client';
export const cache = new InMemoryCache(); // <-- 現在は空のまま

usePersistCachedApolloClient カスタムフックを作成します。

// src/apollo/hooks/usePersistCachedApolloClient.tsx

import {useState, useEffect} from 'react';
import {
  from,
  ApolloClient,
  createHttpLink,
  NormalizedCacheObject,
} from '@apollo/client';
import {persistCache, AsyncStorageWrapper} from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {onError} from '@apollo/client/link/error';
import {cache} from 'apollo/cache';

export const usePersistCachedApolloClient = () => {
  const [client, setClient] = useState<ApolloClient<NormalizedCacheObject>>(
    {} as ApolloClient<NormalizedCacheObject>,
  );
  const [loadingCache, setLoadingCache] = useState<boolean>(true);

  useEffect(() => {
    async function init() {
      persistCache({
        cache,
        storage: new AsyncStorageWrapper(AsyncStorage),
        trigger: 'background',
        maxSize: false, // Unlimited cache size
        debug: __DEV__,
      }).then(() => setLoadingCache(false));

      setClient(
        new ApolloClient({
          uri: 'https://flyby-router-demo.herokuapp.com/'
          cache,
          connectToDevTools: true,
        }),
      );
    }

    init().catch(err => {
      console.log(err);
    });
  }, []);

  return {
    client,
    loadingCache,
  };
};

CachePersistor

CachePersistor は ApolloPersistOptions に加え、persistor.restore() persistor.purge() などより細かく Persistor を制御できることができます。

// CachePersistor

// CachePersistor

export default class CachePersistor<T> {
    log: Log<T>;
    cache: Cache<T>;
    storage: Storage<T>;
    persistor: Persistor<T>;
    trigger: Trigger<T>;
    constructor(options: ApolloPersistOptions<T>);
    persist(): Promise<void>;
    restore(): Promise<void>;
    purge(): Promise<void>;
    pause(): void;
    resume(): void;
    remove(): void;
    getLogs(print?: boolean): Array<LogLine> | void;
    getSize(): Promise<number | null>;
}

useCachePersistorApolloClient カスタムフックを作成します。

// src/apollo/hooks/useCachePersistorApolloClient.tsx

import {useState, useEffect, useCallback} from 'react';
import {ApolloClient,NormalizedCacheObject} from '@apollo/client';
import {CachePersistor, AsyncStorageWrapper} from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {cache} from 'apollo/cache';
import {URL} from 'common/utils';

export const useCachePersistorApolloClient = () => {
  const [client, setClient] = useState<ApolloClient<NormalizedCacheObject>>(
    {} as ApolloClient<NormalizedCacheObject>,
  );
  const [persistor, setPersistor] = useState<
    CachePersistor<NormalizedCacheObject>
  >({} as CachePersistor<NormalizedCacheObject>);

  const clearCache = useCallback(() => {
    if (!persistor) return;
    persistor.purge();
  }, [persistor]);
  // const store = client?.cache?.data?.data;

  useEffect(() => {
    async function init() {
      let newPersistor = new CachePersistor({
        cache,
        storage: new AsyncStorageWrapper(AsyncStorage),
        trigger: 'background',
        maxSize: false, // Unlimited cache size
        debug: __DEV__,
      });
      await newPersistor.restore();
      setPersistor(newPersistor);

      setClient(
        new ApolloClient({
          uri: 'https://flyby-router-demo.herokuapp.com/',
          cache,
          connectToDevTools: true,
        }),
      );
    }

    init().catch(err => {
      console.log(err);
    });
  }, []);

  return {
    client,
    persistor,
    clearCache,
  };
};

動作をテストする

Line 4-7、12-33 番のように追加して両方の動作テストをします。⬇︎

usePersistCachedApolloClient() だけに loadingCache を使用することに注意しましょう!

// app/_layout.tsx

...
import {
  useCachePersistorApolloClient,
  // usePersistCachedApolloClient,
} from 'apollo/hooks';
...

function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const {client, loadingCache} = usePersistCachedApolloClient();
  // const {client} = useCachePersistorApolloClient();

  if (loadingCache || !client) {
  // if (!client) {
    return <ActivityIndicator size={'large'} />;
  }

  client
    .query({
      query: gql`
        query GetLocations {
          locations {
            id
            name
            description
            photo
          }
        }
      `,
    })
    .then(result => console.log(JSON.stringify(result, null, 2)));

TypeError: Cannot read property ‘watchQuery’ of undefined このエラーが発生して解決できない時には、
import {isEmpty} from 'lodash';

if(!client) {...}if(isEmpty(client)){...}に変更してみてください。

※テスト結果:コンソールメッセージ内容 ⬇︎が表示されれば OK です。

 {
  "data": {
    "locations": [
      {
        "__typename": "Location",
        "id": "loc-1",
        "name": "The Living Ocean of New Lemuria",
        "description": "Surviving is usually extremely difficult, especially when nutrients are scarce and you have to choose between growing or reproducing. One species on this planet has developed a nifty method to prepare for this. Once full matured, this species will split into 2 versions of itself and attached to each other, so it's essentially reproducing. Once those 2 are fully grown, they newly grown version will either detach itself if enough nutrients are available or it becomes a storage unit for the original, if nutrients are scarce. If nutrients continue to be scarce, the original will use slowly consume the nutrients in the new version in the hope that new nutrients become available again and it can repeat the cycle.",
        "photo": "https://res.cloudinary.com/apollographql/image/upload/v1644381344/odyssey/federation-course1/FlyBy%20illustrations/Landscape_4_lkmvlw.png"
      },
...

おわりに

persistCacheCachePersistor
どちらを採用するかはプロジェクトのニーズによりますが、書き込みが多かった別のアプリの開発時は CachePersistor を採用し、手動( trigger: false )で保持するタイミング( persistor.persist() )を制御したことがありました。

今回のプロジェクトでは、書き込みの少ないアプリを開発するので、デフォルトの persistCache を採用して様子を見ながらCachePersistorへの移行を判断しようかと思います。

スポンサーリンク

参考文献

Get started with Apollo Client

Integrating with React Native

The GraphQL Guide React Native app

コメント

タイトルとURLをコピーしました