Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Offline Support and Caching in Expo With Custom Queuing

5 min readApr 18, 2026

--

Before we start, a word:

I started working on an AI native Boilerplate for Mobile Development. A solution that has a scalable and clean architecture, with the best approaches to have a great app performance. Adding more AI context and rules, to make the AI hallucinate less, and keep the clean code. You can use it either with Cursor, Antigravity, Claude Code,… and it gives the amazing results as expected. Also, it has most important features that help you launch your app fast <Revenue Cat, Authentificaiton with Supabase, Database at Supabase, Analytics, Design System library with most of the features, Custom onboarding for higher retention, and more,…>

I m now in the Beta version, where i need people to test it, and gather feedback, you will get access to the code base for free, and i m looking for feedbacks.

Check it here: https://aimobilelauncher.com/

Also, I m working on a newsletter for Tech and non tech people, on how to use AI tools for App development: https://aimeetcode.substack.com/. Subscribe here: https://aimeetcode.substack.com/

If you need custom solution, or a mobile development, contact us in :https://casainnov.com/

Introduction

Apps that only work with a reliable connection are fragile in ways that are easy to underestimate. Subway tunnels, airplane mode, flaky mobile networks, backend timeouts — users lose connectivity more often than analytics suggest, partly because failed requests don’t always get logged the same way successful ones do. If your app just spins or shows an error when that happens, you’re training users to close it.

This covers a practical approach: persistent caching with AsyncStorage, network detection, offline-aware data loading, and queuing writes while offline.

TL;DR

  • Cache API responses, not UI state
  • Show cached data immediately, refresh in the background when online
  • Tell users explicitly when they’re offline — don’t let the app just look broken
  • Queue writes while offline, replay on reconnect
  • Reach for WatermelonDB or SQLite only when AsyncStorage actually isn’t enough

The basic mindset shift

Before writing code, the question to settle is: what should the app do when there’s no connection? For most screens the answer is the same — show whatever was cached last, refresh in the background if online, stay usable if not. That covers most of what users actually encounter.

Caching with AsyncStorage

A minimal wrapper with TTL support is enough to start:

// services/cacheService.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const PREFIX = 'cache_';
const DEFAULT_TTL = 1000 * 60 * 60 * 24; // 24h
export async function setCache<T>(key: string, value: T, ttl = DEFAULT_TTL) {
const payload = { value, timestamp: Date.now(), ttl };
await AsyncStorage.setItem(`${PREFIX}${key}`, JSON.stringify(payload));
}
export async function getCache<T>(key: string): Promise<T | null> {
const raw = await AsyncStorage.getItem(`${PREFIX}${key}`);
if (!raw) return null;
const { value, timestamp, ttl } = JSON.parse(raw);
if (Date.now() - timestamp > ttl) return null;
return value;
}

Cache API responses. Don’t cache UI state — open modals, scroll positions, form input. It looks like an optimization and creates subtle sync bugs that are harder to debug than the problem you were solving.

Detecting network status

import NetInfo from '@react-native-community/netinfo';

export function useNetworkStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
return NetInfo.addEventListener(state => {
setIsOnline(Boolean(state.isConnected));
});
}, []);
return { isOnline };
}

Offline-aware data loading

Load cached data first, update from the network in the background:

const loadData = async () => {
const cached = await getCache('posts');
if (cached) setData(cached);

if (isOnline) {
const fresh = await fetchPosts();
setData(fresh);
await setCache('posts', fresh);
}
};

Users see content on first render. If the device is online, fresh data replaces it quietly. If not, the cached version stays. No spinner unless there’s genuinely nothing to show.

Queuing writes

Reading offline is relatively straightforward. Writes are where most apps fail silently — the user submits a form, the request goes nowhere, nothing happens, no feedback. They try again, still nothing. They assume the app is broken.

Get Malik Chohra’s stories in your inbox

Join Medium for free to get updates from this writer.

Queue the action instead of dropping it:

// services/offlineQueue.ts
export async function queueAction(action: any) {
const queue = JSON.parse(
(await AsyncStorage.getItem('offline_queue')) || '[]'
);
queue.push({ action, date: Date.now() });
await AsyncStorage.setItem('offline_queue', JSON.stringify(queue));
}

When connection returns, replay the queue and clear it. The tricky parts — conflict resolution, ordering, deduplication — depend on your backend and data model. The queue itself is always the same.

Tell users when they’re offline

A visible banner is enough: “You’re offline. Showing cached data.” Users who see nothing assume the app is slow or broken. The banner sets the right expectation, and it removes an entire category of support tickets.

Cache invalidation

The temptation is to make this smart. It rarely needs to be:

  • Short TTL for data that changes often
  • Manual invalidation after mutations
  • Clear on logout

When invalidation logic starts getting complicated, the actual problem is usually the API structure — stale endpoints, over-broad response shapes, missing timestamps. Fixing the cache layer instead of the API is a local solution to a global problem.

When to go further

AsyncStorage and a write queue handle most apps. The situations where you actually need more:

  • React Query or RTK Query persistence — if you’re already using either and want cache that survives restarts with minimal extra code
  • WatermelonDB — for large relational datasets that need query performance
  • SQLite — for fully offline-first apps with heavy local writes

These are real solutions for real complexity. They also add real maintenance cost. Start with AsyncStorage and switch when you have a concrete reason, not because the architecture looks more serious.

FAQ

Does Expo support offline mode?
Yes — AsyncStorage, SQLite, and the filesystem APIs are all available without special configuration.

Should I cache everything?
No. Cache what a user would notice if it vanished: list data, recent content, anything they’d want to read offline. Don’t cache errors, volatile data, or anything sensitive.

AsyncStorage or MMKV?
AsyncStorage works for most apps. MMKV is meaningfully faster when you’re reading and writing frequently or storing large payloads. Switch when reads are noticeably slow — not before.

Can users write data while offline?
Yes, with a queue. The write is stored locally, replayed when the connection returns, and the queue is cleared after sync.

--

--