DEV Community

Cover image for Optimizing React Native offline mode using Apollo Cache Persist
Nico Bermudez for EF Go Ahead Tours

Posted on

Optimizing React Native offline mode using Apollo Cache Persist

Having offline mode is extremely important for most mobile apps in the market. For us as a travel business, it is vital. Due to the nature of our industry, a lot of our users will simply not have access to cellular connection or wifi. Therefore, one of the most important aspects of our mobile app is to allow our users to be able to see information about their tour, while they're on tour. We want to be able to have them access their day to day itinerary, their flights or hotels, or any other important information about the tour, at any time.

IMG_3304

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:

useEffect(() => {   
   db.transaction((tx) => {
      tx.executeSql("create table if not exists items (id integer       primary key not null, done int, value text);");    
   }); 
}, []);
Enter fullscreen mode Exit fullscreen mode

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.

const add = (text) => {
   if (text === null || text === "") {
      return false;    
   }     
   db.transaction((tx) => {        
      tx.executeSql("insert into items (done, value) values (0, ?)", [text]);        
      tx.executeSql("select * from items", [], (_, { rows }) =>          console.log(JSON.stringify(rows)));      
   }, null, forceUpdate);  
};
Enter fullscreen mode Exit fullscreen mode

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 us. 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.

import { ApolloClient, InMemoryCache } from "@apollo/client";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AsyncStorageWrapper, CachePersistor } from "apollo3-cache-persist";
import { useEffect } from "react";
const cache = new InMemoryCache({
   // your cache configuration
});
const persistor = new CachePersistor({
   cache,
   storage: new AsyncStorageWrapper(AsyncStorage),
});
// then later when initializing your App
useEffect(() => {
   async function initializeCache() {
      await persistor.restore();
      const client = new ApolloClient({
         // your Apollo Client initialization
      });
      client.onClearStore(async () => {
         await persistor.purge();
      });
   }
   initializeCache();
}, []);
Enter fullscreen mode Exit fullscreen mode

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.

<Button
   title="Sign out"
   onPress={async () => {
      await client.clearStore();
   }}
/>
Enter fullscreen mode Exit fullscreen mode

That's it! Now our travelers are happy on the road being able to access their tour data and now you have your mobile app working for offline mode for all of your data and 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

Thanks for reading 👋

If you have any questions, please feel free to comment below.

We're hiring! If you're interested in pursuing a Career with us, feel free to reach out to me on LinkedIn.

Discussion (0)