Optimizing React Native offline mode using Apollo Cache Persist
Offline mode is extremely important for most mobile apps in the market. Different companies with apps related to travel, weather, gaming, or finance, for example, find it vital to allow users to access certain data anytime, anywhere. With this blog post, we’re gonna explore how to create a seamless offline experience using React Native and Apollo.
Background
In the past, we’ve explored different options, including maybe the most popular approach which is to use SQLite. This is a fine solution, but it can get pretty tedious in terms of extra configuration and needing to read and write manually directly from this database.
In this simple todo app example taken from Expo, we need to manually read and write on initial render:
1useEffect(() => { 2 db.transaction((tx) => {3 tx.executeSql("create table if not exists items (id integer primary key not null, done int, value text);"); 4 }); 5}, []);
Here, we need to initialize our table if we haven’t done so yet.
Same goes for any other function that needs to write to the db. Let’s take a look at adding a todo below.
1const add = (text) => {2 if (text === null || text === "") {3 return false; 4 } 5 db.transaction((tx) => { 6 tx.executeSql("insert into items (done, value) values (0, ?)", [text]); 7 tx.executeSql("select * from items", [], (_, { rows }) => {}, null, forceUpdate);8};
In this function, we are directly calling our database to insert our new todo items, as well as then read them.
While all of this effectively works, you can quickly see how this can become difficult to maintain for larger apps.
Apollo Client
With our current architecture, we have over 40 federated GraphQL micro-services. So we share these API’s amongst all of our clients, including both web and mobile. We consume these API’s using Apollo Client. “Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI.” For the purposes of this blog post, I won’t get into the details of what Apollo accomplishes for us, but you can read more about it in their docs.
One of the main benefits from Apollo is its out of the box caching implementation. They store all of your query results in a local, normalized, in-memory cache. What this means for your mobile app is that if you navigate to a screen, fetch the data, and then return to that screen (all while your app is still open), you won’t need to re-fetch all of that data again from the network. Instead, Apollo is simply smart enough to get that data from the in-memory cache.
There’s a lot of different strategies in the way you should properly cache data using Apollo Client. You can configure how you want to balance fetching data from your cache through Apollo’s fetchPolicies. It’s also very important to make sure your data is being cached properly. Apollo does this by generating a cache ID using a typename and an “id or _id” field on your data object. So it’s vital that you specify a unique identifier on each of your data models, otherwise, you can specify how you plan on merging those objects in your typePolicies.
Apollo Cache Persist
So now we should have our cache properly working, but we notice that once you close out the app, you’ll be forced to fetch from the network again, since Apollo Client simply provides an in-memory cache. Going back to earlier, this won’t work for certain industries. Travelers on the road need to be able to access their data offline, which means once they close out their app, they won’t be able to fetch data from the network and they also won’t have the cache available in memory. So what do we do?
We can use Apollo Cache Persist! Apollo Cache Persist is a library which seamlessly saves and restores your Apollo cache from persistent storage.
The setup is extremely simple. First, we need to decide which configurations we want to add. For example, if we want to change the maxSize, which defaults to 1MB, we can set this here. You can reference the additional configuration here. For our example, we’ll focus on just providing our storage provider, which will be AsyncStorage.
1import { ApolloClient, InMemoryCache } from "@apollo/client";2import AsyncStorage from "@react-native-async-storage/async-storage";3import { AsyncStorageWrapper, CachePersistor } from "apollo3-cache-persist";4import { useEffect } from "react";
1const cache = new InMemoryCache({2 // your cache configuration3});
1const persistor = new CachePersistor({2 cache,3 storage: new AsyncStorageWrapper(AsyncStorage),4});
then later when initializing your app
1useEffect(() => {2 async function initializeCache() {3 await persistor.restore();4 const client = new ApolloClient({5 // your Apollo Client initialization6 });7 client.onClearStore(async () => {8 await persistor.purge();9 });10 }11 initializeCache();12}, []);
First, we set up our initialize our CachePersistor from apollo-cache-persist. We reference our cache by passing it in, and we determine our storage and initialize our AsyncStorageWrapper with AsyncStorage from @react-native-async-storage/async-storage
. Then, in our app initialization, we restore our cache using persistor.restore()
. This takes care of the hard work for us by checking against Async Storage and seeing if we have any data that we need to restore as part of our cache. Once we have this part setup, we can close our app and you’ll notice you have data available without needing to make network requests (Assuming you already fetched at least once before and have it in your cache). You can switch to airplane mode to properly test this out.
Since we have proper authentication, we still have one more step. We need to make sure this data is cleared from storage on logout because of security and to prevent any bugs with data from any other users logging in using the same device. In your app initialization, we can add client.onClearStore(async () => { await persistor.purge() });
, which now allows us to purge our async storage whenever we clear our Apollo Client store. So now, when we sign our users out, we can simply clear the store.
1<Button2 title="Sign out"3 onPress={async () => {4 await client.clearStore();5 }}6/>
That’s it! Now our users are happy on the road being able to access their data making the user experience that much better.
Example Repo
You can find a working demo here. It is made so you can see a working app with the examples we’ve outlined in this blog post.
https://github.com/nicobermudez/expo-offline-app-example