How to Add Offline Support to Your Expo App

DavidDavid August 17, 2026 9 min read
App Store Launch Club - an unplugged ethernet cable beside a closed notebook, representing an app running without a connection
Original image, App Store Launch Club

The Short Answer

To add offline support to an Expo app, do three things in order: cache the data you already fetched so previously seen screens still render, detect connection state and show it plainly instead of spinning forever, and queue the small number of user actions that genuinely must survive a dropout. That covers the behaviour people describe as an app working offline. Full bidirectional sync with conflict resolution is a separate and much larger project, and treating it as step one is why offline support so often stalls.

Decide What Actually Needs to Work Offline

Before writing anything, sort your screens into three groups. This step takes fifteen minutes and it usually removes most of the work, because it turns out only a small part of the app has any business functioning without a connection.

CategoryExamplesWhat to build
Must work offlineSaved items, notes the user wrote, downloaded contentLocal storage as the source of truth, sync when back online
Should show stale dataFeeds, lists, profiles, previously loaded detail screensCached reads plus a clear last updated indicator
Cannot work offlinePayments, login, search against a live index, uploadsDetect the state and explain it rather than failing silently

The third row is where most of the value hides. You are not building anything, you are replacing a hanging request with an honest message. Users forgive a feature that cannot run without a connection and they do not forgive a screen that appears frozen.

Cached Reads Come First

The single highest-return change is persisting data you have already fetched, so that a screen the user has seen before still renders when the network is gone. Without it, every screen resets to empty on relaunch, which is the behaviour that makes an app feel fragile even on a good connection where requests are merely slow.

  1. Persist the responses you already fetch. Write them to local storage on success and read from storage first on mount.
  2. Render the cached version immediately, then refresh in the background. The screen should never be blank while a request is in flight if you already have data.
  3. Show when the data is from. A small last updated line is what converts stale data from a bug into a feature.
  4. Keep the cache small and scoped. A cache of everything the app has ever seen becomes its own problem, and a size limit is easier than a cleanup strategy later.
  5. Decide what expires. Some data is fine indefinitely, some is misleading after an hour, and that judgement is per screen rather than global.

Two practical notes on storage. Small key-value data and cached responses belong in an async storage layer, while anything you will query or filter locally belongs in a real on-device database. And do not put tokens or anything sensitive in plain storage, which is the same boundary discussed in [how to add authentication to your Expo app](/blog/how-to-add-authentication-to-your-expo-app).

Detect the Connection and Say Something

Connection state is not a boolean in practice, and treating it as one causes its own bugs. A device can be connected to a network with no internet access, which is what happens on captive portal wifi in hotels and cafes, and that state produces exactly the hanging requests you were trying to avoid. Check for reachability rather than just for an interface being up.

  • Show a persistent but unobtrusive indicator when offline. A thin bar is enough, a blocking modal is not.
  • Set request timeouts. Without one, a dropped connection is indistinguishable from a slow server, and both look like a frozen screen.
  • Disable actions that cannot possibly work, with the reason attached. A greyed-out button that says needs a connection is far better than one that fails on tap.
  • Retry automatically when the connection returns, and do it quietly. Users should not need to know a retry happened.
  • Never let a failed request leave the UI in a loading state. Every request needs a failure path that ends somewhere.

Queue the Actions That Must Survive

Some user actions cannot simply be blocked, because losing them is losing the user's work. Anything they typed, created, or explicitly saved belongs in this group. The pattern is to write it locally first, mark it as pending, show it in the interface immediately, and send it when the connection returns.

  1. Write to local storage before attempting the network call, so the data exists regardless of the outcome.
  2. Give each queued item a locally generated identifier, so it can be shown, edited, and matched to the server record later.
  3. Show pending items differently. A small indicator is honest and it stops the user wondering whether the action registered.
  4. Send the queue in order when connectivity returns, and remove each item only after the server confirms it.
  5. Make the send idempotent. Retries will happen, so an operation that runs twice must not create two records.
  6. Cap retries and surface a real failure eventually. A queue that retries forever silently is worse than an error message.

Keep this list short deliberately. Every action you queue is a piece of state that can diverge from the server, and each one adds a case you have to reason about. Three queued actions is maintainable, and twenty is a sync engine you did not plan to build.

Why Full Sync Is a Trap Early On

Bidirectional sync means the same record can change in two places while they cannot see each other, and then you have to decide what happens. That decision is a product question rather than a technical one, and it does not have a general answer. Last write wins quietly destroys data. Prompting the user needs an interface for showing two versions of a record. Merging needs field-level rules you have to invent.

The honest scope estimate is that a full sync layer is comparable in size to a significant feature, and it is very rarely what stands between you and shipping. If your app genuinely needs it, reach for an established sync solution rather than writing your own, and either way get the app in the store on cached reads and honest states first.

What to Do This Week

Put the device in airplane mode and open your app cold. Write down every screen that shows a spinner forever, an empty state that looks like a bug, or a button that fails silently. That list is your actual offline backlog, and it is usually much shorter than expected.

Then fix it in one order: honest states first, cached reads second, queued actions last and only for the things you cannot afford to lose. Doing it in that order means every step ships something users notice, which is not true of starting with sync.

Free app-building tips, straight to your inbox

Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.

Frequently asked questions

What is the minimum offline support an app should have?

Cached reads so previously loaded screens still render, and honest connection states instead of indefinite spinners. Those two together cover most of what users describe as an app working offline, and they require no sync logic. Queuing user actions comes third and only for data you cannot afford to lose.

Do I need full data sync to support offline use?

Almost never as a first step. Bidirectional sync means the same record can change in two places that cannot see each other, and resolving that is a product decision with no general answer. It is comparable in size to a significant feature. Ship cached reads and honest states first, and use an established sync solution if you later genuinely need one.

How do I detect whether the device is really online?

Check reachability rather than only whether a network interface is up. A device can be connected to wifi with no internet access, which is common on captive portal networks in hotels and cafes, and that state produces the same hanging requests you were trying to prevent. Combine the connection check with request timeouts so a stalled call always ends somewhere.

Where should offline data be stored in an Expo app?

Small key-value data and cached API responses belong in an async storage layer, while data you need to query or filter locally belongs in an on-device database. Tokens and anything sensitive should not go in plain storage regardless of convenience. Keep the cache scoped and size-limited rather than caching everything the app has ever loaded.

Can an app be rejected for handling offline badly?

Yes. Reviewers do test on poor connections, and an app that hangs, shows an empty screen with no explanation, or fails silently can be rejected for not functioning as expected. Detecting the state and saying so plainly is a review consideration rather than a nice-to-have polish item.

How do I stop queued offline actions creating duplicates?

Make the operation idempotent and give each queued item a locally generated identifier before sending. Retries are inevitable when connectivity is intermittent, so the server needs to be able to recognise a repeat of the same operation rather than creating a second record. Remove items from the queue only after the server confirms them.

Last reviewed by David on August 17, 2026

David

Written by

David

Founder and app builder

Keep reading

App StoreLaunch

App Store Phased Release: How the 7-Day Rollout Works, How to Pause It, and When to Skip It

App Store phased release rolls a version update out to a random sample of users with automatic updates on over 7 days: 1 percent on day one, then 2, 5, 10, 20, 50 and 100. You can pause it for up to 30 days in total, or release to everyone with one button. Here is exactly what Apple says it does, what it does not do, and how I decide when an Expo app update should use it.

David 8 min
Read article
App StoreLaunch

App Store Pre-Order: How to Set It Up for Your First Expo App (and When It Is Worth It)

An App Store pre-order publishes your product page before your release date so people can order the app, and on launch day it downloads to their device automatically. Apple only publishes the pre-order after the build has passed App Review, the release date has to sit two to 180 days out for a brand-new app, and you have to release the version manually. Here is how to set it up in App Store Connect for an Expo app, what Apple charges and when, and the three questions I use to decide whether a pre-order is worth the wait.

David 9 min
Read article

Ready to build it yourself?

Join App Store Launch Club, the #1 community for building and launching apps, for $9/month.

← Back to the blog