Cache API data with a time-to-live, fall back to saved data offline, and invalidate it correctly.
Why it matters
Users open an app expecting to see something immediately, whether they are on fast Wi-Fi, a weak signal or in airplane mode. If every screen waits for the network, the app feels slow online and useless offline. A good caching strategy shows the last known data at once, refreshes it in the background, and is honest when data might be out of date.
Caching strategies
- Network first: try the network; fall back to cache only if it fails. Always fresh when online, but every screen waits.
- Cache first: use the cache if present; hit the network only when there is nothing cached. Fast, but data can go stale forever.
- Stale-while-revalidate: show cached data immediately, then fetch and update. This is the default in TanStack Query and works for most screens.
- Time-to-live (TTL): a cached entry is "fresh" for a set time (say 5 minutes). Fresh entries are used without a request; older ones trigger a refresh, but can still be shown if the refresh fails.
Pick per screen. A product catalog can be minutes stale; an account balance should be refreshed every time and labelled with its "last updated" time.
Worked example: read-through cache with offline fallback
Each cache entry stores the data and when it was saved:
// cache: { [key]: { data, savedAt } }
function readThrough(cache, key, now, ttlMs, fetchFn) {
const entry = cache[key];
if (entry && now - entry.savedAt < ttlMs) {
return { data: entry.data, source: "cache", cache }; // fresh: no request
}
const result = fetchFn(); // { ok, data }
if (result.ok) {
return { data: result.data, source: "network",
cache: { ...cache, [key]: { data: result.data, savedAt: now } } };
}
if (entry) return { data: entry.data, source: "stale", cache }; // offline fallback
return { data: null, source: "error", cache };
}
The source tells the UI what to say: nothing for cache or network, a small "Showing saved data from 10:42" banner for stale, and an error state with a retry button for error. Passing now in, instead of calling Date.now() inside, makes the function easy to test.
In a real app the fetch is asynchronous and the cache is persisted (AsyncStorage, MMKV or SQLite) so it survives restarts. TanStack Query can persist its cache with a persister plugin and pause requests while offline, using NetInfo through its onlineManager.
Writes and sync
Reads are the easy half. For writes:
- Optimistic updates: update the cached data immediately, send the request, and roll back if it fails. TanStack Query's
onMutate/onErrorsupport this. - Outbox queue: if offline, store the change and send it later, in order (you built one in Mobile Development Basics).
- Conflicts: if two devices edit the same record, someone has to win. Common rules are last-write-wins with a server timestamp, or a version number that makes the server reject stale writes (HTTP 409) so the app can ask the user.
- Sync engines: for data-heavy offline apps, libraries such as WatermelonDB or PowerSync keep a local database in sync with the server. They are worth it when offline is the main use case, not for a few cached lists.
Invalidation
"There are only two hard things in computer science: cache invalidation and naming things." In practice:
- after a mutation, invalidate the affected keys (
queryClient.invalidateQueries({ queryKey: ["orders"] })), - refetch when the app returns to the foreground or the network reconnects,
- include the user id in cache keys, and clear the cache on sign-out so the next person on a shared device does not see someone else's data.
Common mistakes
- No TTL, so the cache is used forever and users never see new data.
- Throwing away cached data when a refresh fails, leaving a blank screen offline.
- Hiding staleness: a balance from yesterday shown as if it were current.
- Forgetting to clear user data on sign-out.
- Caching everything; some data (one-time codes, payment status) must always be fetched.
Quick recap
- Choose a strategy per screen; stale-while-revalidate with a TTL suits most.
- On failure, fall back to cached data and say that it is saved data.
- Invalidate after writes, refetch on reconnect and clear the cache on sign-out.
Try it
In the Do tab you will write readThrough and isFresh, the core of a cache with offline fallback, and test them with a fake fetch.