1. AsyncStorage Basics & Limits
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
async function saveDraft(text: string) {
await AsyncStorage.setItem('draft-note', text);
}
async function loadDraft() {
return await AsyncStorage.getItem('draft-note'); // string | null
}
async function saveObject(key: string, value: object) {
await AsyncStorage.setItem(key, JSON.stringify(value));
}
AsyncStorage is a flat, unencrypted, string-only key-value store — everything
written to it, including anything you JSON.stringify into it, is
readable in plain text by anyone with access to the device's file system. It's the
right tool for drafts, cached lists, and UI preferences; it is not
the right tool for anything sensitive.
2. SecureStore for Sensitive Data
expo-secure-store uses the platform's real encrypted storage — the
iOS Keychain and Android's Keystore-backed EncryptedSharedPreferences —
for anything that actually needs protecting.
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
async function saveToken(token: string) {
await SecureStore.setItemAsync('auth-token', token);
}
async function getToken() {
return await SecureStore.getItemAsync('auth-token');
}
async function clearToken() {
await SecureStore.deleteItemAsync('auth-token');
}
SecureStore has a much smaller per-value size limit than AsyncStorage (around 2KB on iOS) — it's built for tokens and credentials, not for caching a JSON response.
3. Structured Data with expo-sqlite
Once data has real structure and needs to be queried — a list of notes with tags and dates, not just one blob — a real embedded SQL database beats hand-rolled JSON parsing on every read.
npx expo install expo-sqlite
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabaseSync('notes.db');
export function initDb() {
db.execSync(
'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL, body TEXT, created_at INTEGER);'
);
}
export function addNote(title: string, body: string) {
db.runSync(
'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?);',
[title, body, Date.now()]
);
}
export function getAllNotes() {
return db.getAllSync('SELECT * FROM notes ORDER BY created_at DESC;');
}
The synchronous *Sync methods shown here run on the JS thread and are
fine for typical note-taking-app volumes; for large datasets or heavier queries,
expo-sqlite also exposes async equivalents that don't block rendering.
4. Choosing the Right Storage
| Kind of data | Use |
|---|---|
| UI preferences, drafts, cached API responses | AsyncStorage |
| Auth tokens, API keys, anything secret | SecureStore |
| Structured, queryable records (notes, transactions, offline-first data) | expo-sqlite |
These aren't mutually exclusive — a real app typically uses all three at once: a refresh token in SecureStore, a "last opened tab" preference in AsyncStorage, and the user's actual content in SQLite.
5. Hands-on Exercise
Build an offline notes app with a secure "remember me"
Combine all three storage types in one small app, each used for what it's actually good at.
Requirements:
- A notes screen backed by expo-sqlite: create, list (newest first), and delete notes, each with a title and body.
- A "remember me" toggle on a fake sign-in screen that, when on, stores a mock session token in SecureStore and restores the signed-in state on relaunch.
- A UI preference (e.g. sort order, or dark/light override) stored in AsyncStorage and restored on launch.
- Confirm behavior on force-quit and relaunch: notes persist, remember-me restores the session (or doesn't, if it wasn't checked), and the preference is remembered.
- A short comment in the code next to each storage call explaining why that storage type was chosen for that data.
If db.getAllSync throws about a missing table, confirm initDb() actually ran before any query — it's easy to call it after the first screen has already tried to read, especially if it's tucked inside a useEffect that fires later than a sibling component's own data-loading effect.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is AsyncStorage the wrong place to store an auth token, even though it can technically hold a string of any content?
Why is AsyncStorage the wrong place to store an auth token, even though it can technically hold a string of any content?
AsyncStorage is unencrypted plain-text storage on disk — anything written to it, including a token, is readable by anyone with file-system access to the device (more of a risk than it sounds, especially on rooted/jailbroken devices). SecureStore backs onto the platform's real encrypted storage (Keychain/Keystore), which is what tokens and credentials are meant to use.
Q2
Why would you reach for expo-sqlite instead of just storing a JSON array of notes in AsyncStorage?
Why would you reach for expo-sqlite instead of just storing a JSON array of notes in AsyncStorage?
A JSON blob in AsyncStorage has to be parsed and re-serialized in full on every read and write, and any filtering or sorting happens in JS after loading everything into memory. SQLite lets the database itself query, filter, and sort — ORDER BY, WHERE — which scales far better once there's more than a handful of records.
Q3
What's the practical size limit that pushes you away from SecureStore for larger values?
What's the practical size limit that pushes you away from SecureStore for larger values?
SecureStore is built for small secrets like tokens, with a per-value size limit around 2KB on iOS — trying to cache a full JSON API response or a list of records in it will fail or behave unreliably. AsyncStorage (for non-sensitive data) or SQLite (for structured data) have no such tight ceiling.
Q4
In the notes app exercise, why store the "remember me" token separately from the notes themselves?
In the notes app exercise, why store the "remember me" token separately from the notes themselves?
The token is a credential — exactly the kind of sensitive value SecureStore exists for — while the notes are the user's actual structured content, which belongs in a real queryable store. Mixing them into one storage mechanism would either under-protect the token or over-engineer storage for data that doesn't need encryption.