この記事は 2023 年 10 月時点の記録です(2026 年 7 月 追記)
キャッシュを永続化してオフラインでも表示できるようにする、という考え方は変わりません。
apollo3-cache-persistも、Apollo の公式ドキュメントで今も推奨ライブラリとして案内されています。ただしApollo Client 本体がメジャーバージョンを跨ぎました。あわせて、本文のコードで直しておきたい点もお伝えします。
1. Apollo Client 4 が出ています
2025 年 8 月に
@apollo/clientの 4.0 が公開されました。構造が整理され、バンドルサイズが 20〜30% ほど小さくなっています。本文は 3 系を前提としているため、そのままでは動かない箇所が出ます。主な変更点です。
- import のパスが分かれました — React 向けとコア向けで読み込み先が変わります
dataStateという新しい API が入りました- エラーのクラスが変わりました
- RxJS が peer dependency になりました。別途インストールが必要です
公式に移行ガイドと codemod(自動書き換えツール)が用意されていて、「数分で 9 割は移行できる」と案内されています。手で全部直す必要はありません。
すぐに上げる必要がなければ 3 系のままでも動きますが、新しく始めるなら 4 系からのほうがよいと思います。
2. デモ用のエンドポイントについて
本文の動作確認で使っている
herokuapp.comのデモ URL は、現在は応答しない可能性があります。Heroku の無料プランが終了しているためです。接続できない場合、原因は設定ではなく接続先です。Apollo の公式チュートリアルで案内されている現行のエンドポイントに差し替えてお試しください。
3. client の初期値を空オブジェクトにしない
本文のカスタムフックでは、次のように書いています。
const [client, setClient] = useState<ApolloClient<NormalizedCacheObject>>( {} as ApolloClient<NormalizedCacheObject>, );
{} as ApolloClientは、中身が空なのに「ApolloClient である」と型に嘘をついている状態です。準備が終わる前に誰かがclient.query()を呼ぶと、その場で落ちます。しかも TypeScript は何も警告してくれません。const [client, setClient] = useState<ApolloClient<NormalizedCacheObject> | null>(null);
nullを初期値にすれば、使う側で null チェックが必須になります。本文でもif (loadingCache || !client)でチェックしているので、実質的な動きは同じです。型が実態に合うぶんだけ安全になります。4. レンダー中に query を呼ばない
「動作をテストする」の箇所で、コンポーネントの中で直接
client.query()を呼んでいます。動作確認としては分かりやすいのですが、この書き方だと再描画のたびにクエリが飛びます。そのまま製品コードに残さないでください。実際にデータを取るときは
useQueryを使います。読み込み状態やエラーも一緒に扱えます。一度だけ実行したい処理であればuseEffectの中に置いてください。※ 1 は 2026 年 7 月時点の公開情報に基づきます。2 は可能性の指摘であり、実際に接続を試したものではありません。3・4 は本文に掲載したコードを読み直して整理したものです。
開発環境
この記事の環境の下で行います。
一箇所にまとめて置きたいので下記のように 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
デフォルトの persistCache は ApolloPersistOptions ( 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"
},
...
おわりに
persistCache?CachePersistor?
どちらを採用するかはプロジェクトのニーズによりますが、書き込みが多かった別のアプリの開発時は CachePersistor を採用し、手動( trigger: false )で保持するタイミング( persistor.persist() )を制御したことがありました。
今回のプロジェクトでは、書き込みの少ないアプリを開発するので、デフォルトの persistCache を採用して様子を見ながらCachePersistorへの移行を判断しようかと思います。
コメント