AsyncStorage is the standard way to store small amounts of persistent data in a React Native or Expo application. It works like localStorage in a browser, a key-value store that survives app restarts, but it is asynchronous, meaning every read and write returns a Promise rather than blocking the thread.

It is the correct tool for: user preferences, authentication tokens, draft content, cached responses, and any state you want to survive a restart. It is the wrong tool for: large datasets, relational data, binary files, or anything over a few megabytes.

A person in a dark fitting room before a tall ornate mirror: private, immediate feedback, safe from public view. Local state, visible only to you.
AsyncStorage is the fitting room. Private, immediate, on-device. Nobody sees what is stored here until you choose to send it to the server.

How it works

AsyncStorage stores data as plain strings. Every value you store must be serialised (converted to a string), and every value you retrieve must be deserialised (parsed back). The most common pattern is JSON.stringify on write and JSON.parse on read.

typescript
import AsyncStorage from '@react-native-async-storage/async-storage';

// Write
await AsyncStorage.setItem('user_name', JSON.stringify({ name: 'Linda' }));

// Read
const raw = await AsyncStorage.getItem('user_name');
const data = raw ? JSON.parse(raw) : null;

// Delete
await AsyncStorage.removeItem('user_name');

// Clear everything
await AsyncStorage.clear();

Storage architecture

Your App Code
AsyncStorage.setItem() AsyncStorage.getItem() Returns a Promise. always await it
React Native Bridge
Serialisation layer Converts JS values to native storage calls
Native Storage
iOS: NSUserDefaults / SQLite Android: SharedPreferences / SQLite Platform-specific, but identical API surface

Installation

With Expo:

bash
npx expo install @react-native-async-storage/async-storage

Without Expo (bare React Native):

bash
npm install @react-native-async-storage/async-storage
npx pod-install  # iOS only

Common patterns

Pattern 1: Persisting user settings

typescript
const saveSettings = async (theme: 'light' | 'dark', language: string) => {
  const settings = { theme, language, updatedAt: new Date().toISOString() };
  await AsyncStorage.setItem('app_settings', JSON.stringify(settings));
};

const loadSettings = async () => {
  const raw = await AsyncStorage.getItem('app_settings');
  return raw ? JSON.parse(raw) : { theme: 'dark', language: 'en' };
};

Pattern 2: Zustand persistence middleware

The most common production pattern is combining AsyncStorage with Zustand’s persist middleware. This means your entire app state survives restarts automatically:

typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

const useStore = create(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    }),
    {
      name: 'wardrobe-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

Pattern 3: Storing auth tokens

typescript
const AUTH_KEY = '@auth_token';

export const saveToken = (token: string) =>
  AsyncStorage.setItem(AUTH_KEY, token);

export const getToken = () =>
  AsyncStorage.getItem(AUTH_KEY);

export const clearToken = () =>
  AsyncStorage.removeItem(AUTH_KEY);

What to store and what not to store

Store hereDon’t store here
User preferences (theme, language)Passwords in plaintext
Auth tokens (use Expo SecureStore instead for sensitive data)Large datasets (>1MB)
Cached API responsesBinary files or images
Draft contentAnything requiring querying or indexing
App state for Zustand persistRelational data with joins
Onboarding completion flagsData shared across users

Storage limits

AsyncStorage does not enforce a hard limit in the library itself, but the underlying platforms do:

  • iOS: No official limit, but performance degrades above ~6MB per key
  • Android: Defaults to 6MB total; can be raised in native config
  • Expo Go: Additional restrictions may apply in development

For anything beyond simple key-value data, use a proper embedded database (WatermelonDB , SQLite via Expo) or offload to the cloud.


AsyncStorage vs alternatives

OptionUse caseSize limitPersistenceSearchable
AsyncStorageApp preferences, tokens, draft state~6MBYesNo
Expo SecureStoreSecrets, auth tokens (encrypted)2KB per valueYesNo
SQLite (Expo)Structured relational dataDisk spaceYesYes
WatermelonDBLarge offline-first datasetsDisk spaceYesYes
MMKVHigh-performance key-value (C++)LargeYesNo
In-memory (Zustand)Session state, UI stateRAMNoNo

When to upgrade from AsyncStorage

If you find yourself storing more than 10 keys, doing substring searches on stored values, or managing complex data relationships, stop and move to SQLite or a proper backend.


Error handling

AsyncStorage operations can fail. Always handle errors in production code:

typescript
const loadData = async () => {
  try {
    const value = await AsyncStorage.getItem('my_key');
    return value ? JSON.parse(value) : null;
  } catch (error) {
    console.error('AsyncStorage read failed:', error);
    return null; // Graceful fallback
  }
};

Performance considerations

AsyncStorage is asynchronous by design, it will not block your UI thread. However:

  • Multiple reads: If you read 20 separate keys on app launch, consider using AsyncStorage.multiGet() instead
  • Large objects: Storing a 500-item list as a single JSON blob works, but updates require rewriting the entire blob
  • Frequency: Writing on every keystroke is too frequent: debounce writes by 500-1000ms

Expo SecureStore: when security matters

For auth tokens and any data that should be encrypted at rest, use expo-secure-store instead. It uses the platform’s secure enclave (iOS Keychain, Android Keystore):

typescript
import * as SecureStore from 'expo-secure-store';

await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');

SecureStore is limited to 2KB per value. Use it for tokens and secrets, not for large cached data.


Further reading