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

Data sync and caching

Reading41mLesson 2 of 3 in this module

What you'll learn

  • Cache API data with an offline fallback
  • Choose a caching strategy per screen, such as stale-while-revalidate with a TTL
  • Invalidate cached data after writes and clear it on sign-out
  • Describe optimistic updates, outbox queues and conflict rules for writes

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:

JavaScript
// 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.

Key terms (5)

Stale-while-revalidate
A caching strategy that shows cached data immediately, then fetches fresh data and updates the screen.
Time-to-live
How long a cached entry counts as fresh; after that it triggers a refresh but can still be shown if the refresh fails.
Optimistic updates
Changing the cached data immediately when the user acts, sending the request, and rolling back if it fails.
Outbox queue
A store of changes made offline that are sent to the server later, in order.
Invalidation
Marking cached data as out of date so it is refetched, for example after a mutation or on reconnect.

Recap

  • Pick a caching strategy per screen; stale-while-revalidate with a TTL suits most screens.
  • A read-through cache returns fresh entries without a request and refreshes older ones.
  • When a refresh fails, show the saved data and say that it is saved data.
  • Handle writes with optimistic updates, an outbox queue and a clear conflict rule.
  • Invalidate after writes, refetch on reconnect and clear the cache on sign-out.
Also in this lesson: