Skip to main content
Back to module
Mobile App Production · App Architecture

Data sync and caching

Reading41mLesson 2 of 3 in this module

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 / onError support 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.

Also in this lesson: