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

Ayomide DanielAyomide Daniel
5 min read

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 means your app can:

  • Load data instantly from cache, even when the network is down

  • Queue and retry mutations after reconnecting

  • Provide a seamless user experience despite connectivity changes

Here’s why this approach is worth investing in:

  • Better UX: Users can still interact with your app while offline

  • Faster performance: Cached data loads instantly

  • More reliable: Your app doesn’t freeze or error out when network drops

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

  • User trust: Users trust apps that don’t break when the internet does

Apollo Client makes this possible with its in-memory cache, retry capabilities, and a flexible API.

Tech Stack Overview

To build this offline-ready experience, we’ll use:

  • React Native + Expo: Quick setup and native-like development

  • Apollo Client: Fetching and caching GraphQL data

  • @react-native-async-storage/async-storage: Persisting cache across sessions

  • expo-network: Checking connectivity status

1. Set Up Apollo Client with Cache Persistence

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

npx expo install @react-native-async-storage/async-storage expo-network
npm install @apollo/client graphql apollo3-cache-persist

Set up Apollo Client and enable cache persistence:

// lib/apollo.ts
import {
  ApolloClient,
  InMemoryCache,
  createHttpLink,
} 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://graphqlzero.almansi.me/api' });

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

  useEffect(() => {
    (async () => {
      try {
        await persistCache({ cache, storage: AsyncStorage });
        const apolloClient = new ApolloClient({ link, cache });
        setClient(apolloClient);
      } catch (e) {
        console.error('Apollo cache persistence failed:', e);
      }
    })();
  }, []);

  return client;
};

2. Detect Network Status with expo-network

Create a simple utility to check if user’s device is online:

// lib/network.ts
import * as Network from 'expo-network';

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

This allows you to toggle between online/offline states for UX decisions like form disabling if offline or mutation queuing.

3. Build the UI with Apollo & Offline Banner

We’ll fetch user data and display an offline banner if the network drops:

// lib/gql/queries.ts
import { gql } from '@apollo/client';

export const GET_USER = gql`
  query {
    user(id: 1) {
      id
      name
      email
    }
  }
`;
// components/OfflineBanner.tsx
import { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import { checkOnline } from '../lib/network';

export const OfflineBanner = () => {
  const [isOffline, setOffline] = useState(false);

  useEffect(() => {
    const ping = async () => setOffline(!(await checkOnline()));
    ping();
    const interval = setInterval(ping, 5000);
    return () => clearInterval(interval);
  }, []);

  if (!isOffline) return null;

  return (
    <View style={{ backgroundColor: '#F6F8FA', padding: 8 }}>
      <Text style={{ color: 'blue' }}>You are offline</Text>
    </View>
  );
};
// app/index.tsx
import { ApolloProvider, useQuery } from '@apollo/client';
import { ActivityIndicator, Text, View } from 'react-native';
import { OfflineBanner } from '../components/OfflineBanner';
import { useApollo } from '../lib/apollo';
import { GET_USER } from '../lib/gql/queries';

function Home() {
  const { data, loading } = useQuery(GET_USER);

  if (loading) return <ActivityIndicator />;

  return (
    <View style={{ padding: 16, justifyContent: 'center', alignItems: 'center' }}>
      <Text style={{ fontSize: 18 }}>{data?.user?.name}</Text>
      <Text>{data?.user?.email}</Text>
    </View>
  );
}

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

  if (!client) return <Text>Loading Apollo...</Text>;

  return (
    <ApolloProvider client={client}>
      <OfflineBanner />
      <Home />
    </ApolloProvider>
  );
}

4. UX Considerations

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

  • Skeleton loaders when fetching (even cached) data

  • Visual indicators like a toasts or banners to show offline status

  • Retry buttons for failed requests

  • Optimistic UI for offline form submissions

  • 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 data too aggressively unless needed

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

  • Checking connectivity with expo-network

  • Building UX patterns that adapt to connectivity changes

Apps that work offline feel faster, more stable, and more reliable even without a perfect network. Build them right and users will notice (and return).

Want to see the full source code? Check out the GitHub repo

1
Subscribe to my newsletter

Read articles from Ayomide Daniel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayomide Daniel
Ayomide Daniel

Frontend Engineer specializing in React Native and Next.js. I build performant mobile and web apps that feel smooth, load fast, and scale well Currently exploring offline-first architecture, intuitive UI/UX patterns, and shipping client-ready code that solves real problems