Skip to main content
Back to module
Mobile Development Basics · Mobile UX and Device Features

Storage and offline

Reading35mLesson 2 of 3 in this module

Persist data on the device and queue changes so the app keeps working offline.

Why it matters

Phones lose connectivity all the time: lifts, tunnels, flights, patchy rural signal. Users also expect an app to remember things: their settings, the draft they were writing, the list they downloaded yesterday. An app that forgets everything on restart, or shows a spinner forever offline, feels broken. Local storage and a little offline thinking fix both.

Your storage options

OptionGood forNotes
@react-native-async-storage/async-storageSmall key-value data: settings, flags, small caches, draftsAsynchronous, strings only, not encrypted
expo-sqliteLarger structured data you queryReal SQL database on the device
expo-secure-storeSecrets: tokens, credentialsUses the iOS Keychain and Android Keystore; small values only
expo-file-systemFiles: downloads, imagesPaths inside the app's sandbox

Rule of thumb: settings and small JSON go in AsyncStorage, lots of rows go in SQLite, and anything secret goes in SecureStore. Never put tokens or passwords in AsyncStorage.

Worked example: remembering settings

AsyncStorage stores strings, so you serialize with JSON and always guard the parse:

import AsyncStorage from "@react-native-async-storage/async-storage";

const KEY = "settings:v1";
const DEFAULTS = { theme: "system", reminders: true };

export async function loadSettings() {
  try {
    const raw = await AsyncStorage.getItem(KEY);
    return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : DEFAULTS;
  } catch {
    return DEFAULTS;              // corrupt or unreadable: fall back, do not crash
  }
}

export async function saveSettings(settings) {
  await AsyncStorage.setItem(KEY, JSON.stringify(settings));
}

Three habits are visible here. The key has a version (v1) so a future format change can migrate cleanly. Loading merges over defaults, so a new setting added in a later release still has a value. And a bad value falls back rather than crashing the app on launch, which would be impossible for the user to fix.

Install it with npx expo install @react-native-async-storage/async-storage. It works in Expo Go.

Offline first, in one idea

"Offline first" means the app reads from and writes to the device first, and syncs with the server when it can. For reading, show cached data immediately and refresh in the background (you will build this properly in Mobile App Production). For writing, use an outbox queue:

  1. When the user does something (adds a note, likes a post), update the UI at once and add an action to a queue saved in storage.
  2. When the network is available, send queued actions in order.
  3. Remove an action only after the server confirms it. If one fails, stop and try again later, so later actions do not overtake earlier ones.
queue: [add "Buy milk", add "Call Sam", delete "Old note"]
send #1 ok  -> remove
send #2 fails (offline) -> stop; queue is now [add "Call Sam", delete "Old note"]

The @react-native-community/netinfo library tells you when connectivity changes, so you can flush the queue when it comes back. Give each action an id, so if a response is lost and you send it twice, the server can recognise the duplicate.

Common mistakes

  • Storing secrets in AsyncStorage. It is plain text on the device. Use SecureStore.
  • Unguarded JSON.parse. One corrupt value crashes the app at startup forever.
  • Saving on every keystroke. Debounce saves, or save when the screen loses focus.
  • Dropping failed actions. If a send fails, keep the action and retry; users assume what they saw on screen was saved.
  • Treating "connected to Wi-Fi" as "online". A captive portal or a down server still fails. Handle request errors even when NetInfo says you are connected.

Quick recap

  • AsyncStorage for small key-value data (strings, JSON), SQLite for lots of structured data, SecureStore for secrets.
  • Version your keys, merge over defaults and guard every parse.
  • Offline writes go into an ordered outbox queue that is flushed when the network returns and only shrinks on success.

Try it

In the Do tab you will build the outbox: enqueue, flushQueue and safe parseQueue, as pure functions you can test without a phone.

Also in this lesson: