Build React Native Apps: Offline-First Approach Using GraphQL and Caching

Building apps that work offline isn’t just a nice-to-have anymore, it’s essential. Network issues are real, especially in places with unstable connections. Whether it's users in low-connectivity areas or those who just want snappy experiences, handling offline gracefully is one of the most impactful upgrades you can bring to your mobile app. In this guide, we'll walk through how to build an offline-first experience in a React Native app using Expo, GraphQL (via Apollo Client), and efficient local caching strategies that help your app shine even when the connection doesn’t. Why Offline-First Matters Users expect apps to "just work," even when the internet doesn’t. An offline-first approach ensures that your app can not only read data when offline but also queue mutations while offline and sync when back online. Here’s why investing in offline support is a great move: Better UX: Users can interact with the app even with poor/no internet Faster perceived performance: Cached data loads instantly Increased reliability: Your app doesn’t freeze or crash when connectivity drops Wider market reach: Works better in emerging markets with spotty networks User trust: Offline access builds confidence that your app "just works." Apps that function well offline are often described by users as “smooth,” even if they don’t consciously recognize what’s different. Apollo Client makes this possible out of the box with its normalized cache and retry link system. Tech Stack Overview To make this all work smoothly, we’re combining the following tools: React Native with Expo: Fast setup, consistent development experience Apollo Client: Handling GraphQL queries and mutations @react-native-async-storage/async-storage: To store persistent cache data expo-network: For detecting network availability This combination keeps things flexible, lightweight, and scalable. 1. Set Up Apollo Client with Cache Persistence First, let’s install the packages we’ll need:: expo install @react-native-async-storage/async-storage expo-network npm install @apollo/client graphql Now create a helper to initialize Apollo with cache persistence: // lib/apollo.ts import { ApolloClient, InMemoryCache, createHttpLink, ApolloProvider, } from '@apollo/client'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { persistCache } from 'apollo3-cache-persist'; import { useEffect, useState } from 'react'; const cache = new InMemoryCache(); const link = createHttpLink({ uri: 'https://your-graphql-api.com/graphql' }); export const useApollo = () => { const [client, setClient] = useState(null); useEffect(() => { (async () => { await persistCache({ cache, storage: AsyncStorage, }); const apolloClient = new ApolloClient({ link, cache, connectToDevTools: true, }); setClient(apolloClient); })(); }, []); return client; }; Then use this client to wrap your app: // App.tsx import { ApolloProvider } from '@apollo/client'; import { useApollo } from './lib/apollo'; import { View, Text } from 'react-native'; export default function App() { const client = useApollo(); if (!client) return Loading Apollo...; return ( {/* Your app components here */} ); } This setup ensures Apollo’s in-memory cache gets persisted between app sessions using AsyncStorage. 2. Detect Network Status with expo-network Knowing whether the user is online or offline lets you switch behavior dynamically. Use this to switch between online and offline logic. import * as Network from 'expo-network'; const isOnline = async () => { const status = await Network.getNetworkStateAsync(); return status.isConnected && status.isInternetReachable; }; You can use this to conditionally show offline banners, queue or defer mutations, disable form submissions if offline. Even simple checks like these can dramatically improve perceived quality. 3. Reading and Writing from the Cache When offline, direct cache access helps keep things smooth. Apollo Client gives you methods like readQuery and writeQuery for this purpose. const cachedData = client.readQuery({ query: GET_USER }); client.writeQuery({ query: GET_USER, data: newUserData }); This allows components to pull and push to the cache without requiring a network trip. Great for implementing things like: Offline profile editing Caching form drafts Preloading views from previous sessions To go even further, you can set up a local mutation queue and retry logic that flushes once the device is back online. 4. UX Considerations Going offline-first isn’t just technical, it’s also about user experience. Consider: Skeleton loaders when fetching (even cached) data Retry buttons when fetch or save fails Visual indicators like a toast or banner when offline Degraded but useful experiences, not blank screens

May 14, 2025 - 15:22
 0
Build React Native Apps: Offline-First Approach Using GraphQL and Caching

Building apps that work offline isn’t just a nice-to-have anymore, it’s essential. Network issues are real, especially in places with unstable connections. Whether it's users in low-connectivity areas or those who just want snappy experiences, handling offline gracefully is one of the most impactful upgrades you can bring to your mobile app.

In this guide, we'll walk through how to build an offline-first experience in a React Native app using Expo, GraphQL (via Apollo Client), and efficient local caching strategies that help your app shine even when the connection doesn’t.

Why Offline-First Matters

Users expect apps to "just work," even when the internet doesn’t. An offline-first approach ensures that your app can not only read data when offline but also queue mutations while offline and sync when back online.

Here’s why investing in offline support is a great move:

  • Better UX: Users can interact with the app even with poor/no internet

  • Faster perceived performance: Cached data loads instantly

  • Increased reliability: Your app doesn’t freeze or crash when connectivity drops

  • Wider market reach: Works better in emerging markets with spotty networks

  • User trust: Offline access builds confidence that your app "just works."

Apps that function well offline are often described by users as “smooth,” even if they don’t consciously recognize what’s different.

Apollo Client makes this possible out of the box with its normalized cache and retry link system.

Tech Stack Overview

To make this all work smoothly, we’re combining the following tools:

  • React Native with Expo: Fast setup, consistent development experience

  • Apollo Client: Handling GraphQL queries and mutations

  • @react-native-async-storage/async-storage: To store persistent cache data

  • expo-network: For detecting network availability

This combination keeps things flexible, lightweight, and scalable.

1. Set Up Apollo Client with Cache Persistence

First, let’s install the packages we’ll need::

expo install @react-native-async-storage/async-storage expo-network
npm install @apollo/client graphql

Now create a helper to initialize Apollo with cache persistence:

// lib/apollo.ts
import {
  ApolloClient,
  InMemoryCache,
  createHttpLink,
  ApolloProvider,
} from '@apollo/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { persistCache } from 'apollo3-cache-persist';
import { useEffect, useState } from 'react';

const cache = new InMemoryCache();
const link = createHttpLink({ uri: 'https://your-graphql-api.com/graphql' });

export const useApollo = () => {
  const [client, setClient] = useState | null>(null);

  useEffect(() => {
    (async () => {
      await persistCache({
        cache,
        storage: AsyncStorage,
      });
      const apolloClient = new ApolloClient({
        link,
        cache,
        connectToDevTools: true,
      });
      setClient(apolloClient);
    })();
  }, []);

  return client;
};

Then use this client to wrap your app:

// App.tsx
import { ApolloProvider } from '@apollo/client';
import { useApollo } from './lib/apollo';
import { View, Text } from 'react-native';

export default function App() {
  const client = useApollo();

  if (!client) return Loading Apollo...;

  return (
    
      {/* Your app components here */}
    
  );
}

This setup ensures Apollo’s in-memory cache gets persisted between app sessions using AsyncStorage.

2. Detect Network Status with expo-network

Knowing whether the user is online or offline lets you switch behavior dynamically. Use this to switch between online and offline logic.

import * as Network from 'expo-network';

const isOnline = async () => {
  const status = await Network.getNetworkStateAsync();
  return status.isConnected && status.isInternetReachable;
};

You can use this to conditionally show offline banners, queue or defer mutations, disable form submissions if offline. Even simple checks like these can dramatically improve perceived quality.

3. Reading and Writing from the Cache

When offline, direct cache access helps keep things smooth. Apollo Client gives you methods like readQuery and writeQuery for this purpose.

const cachedData = client.readQuery({ query: GET_USER });
client.writeQuery({ query: GET_USER, data: newUserData });

This allows components to pull and push to the cache without requiring a network trip. Great for implementing things like:

  • Offline profile editing

  • Caching form drafts

  • Preloading views from previous sessions

To go even further, you can set up a local mutation queue and retry logic that flushes once the device is back online.

4. UX Considerations

Going offline-first isn’t just technical, it’s also about user experience. Consider:

  • Skeleton loaders when fetching (even cached) data

  • Retry buttons when fetch or save fails

  • Visual indicators like a toast or banner when offline

  • Degraded but useful experiences, not blank screens

  • Manual refresh to revalidate stale cache

If users can’t notice when they’ve gone offline, you’ve done something right

5. Tips

  • Apollo’s in-memory cache is volatile unless you persist it manually.

  • Default fetch policies like cache-first or cache-and-network can drastically reduce re-renders

  • Always assume mutations will fail when offline. Don’t rely on automatic retries

  • Logging cache writes during development helps debug data flow

  • Not every query needs persistence. Be selective

  • Avoid invalidating cache too aggressively. Let it breathe

Conclusion

Making your React Native app offline-ready using Apollo Client and caching techniques isn’t just about edge cases, it’s about delivering a consistently smooth user experience. With a bit of setup, you can enable your app to handle flaky networks with grace.

Focus on:

  • Persisting meaningful cache to avoid unnecessary re-fetches

  • Detecting network status reliably

  • Building clean UX patterns around connectivity changes

Apps that work offline feel faster, more stable, and more thoughtful. Users love them and they keep coming back

Want to see all of this in action? Check out the full source code on GitHub