Shipnative
ShipnativeShipnative
Sign in
GuideAugust 2026 · 10 min read

React Native AsyncStorage: The Setup That Survives

AsyncStorage is the first persistence most React Native apps reach for, and the API is small enough to learn in one sitting: strings in, strings out, everything a promise. The interesting part is not the API — it is the four or five ways a naive integration goes wrong in production. A logged-out flash on every cold start. An Android-only failure at 6 MB. A settings screen that writes twelve keys in a loop. This is the version that holds up, plus an honest map of when you should be using something else entirely.

Install: it is not in React Native anymore

Half the tutorials still on the internet open with import { AsyncStorage } from 'react-native'. That export was deprecated and then removed. The maintained implementation is a community package:

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

# bare React Native:
npm install @react-native-async-storage/async-storage && npx pod-install

Use npx expo install rather than npm install in an Expo project — it resolves the version that matches your SDK instead of the newest one on npm, which is the single most common cause of a native module that builds fine and crashes on launch.

Wrap it once, in a typed module

Every screen calling the raw API means JSON.parse scattered through your codebase and a key typo waiting to become a silent data-loss bug. One module fixes both:

// lib/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

export const KEYS = {
  settings: 'settings.v1',
  draft: 'compose.draft.v1',
  lastSync: 'sync.lastAt.v1',
} as const;

export async function load<T>(key: string, fallback: T): Promise<T> {
  try {
    const raw = await AsyncStorage.getItem(key);
    return raw == null ? fallback : (JSON.parse(raw) as T);
  } catch {
    // Corrupt value or a schema change. Do not crash the app over a cache.
    await AsyncStorage.removeItem(key);
    return fallback;
  }
}

export async function save(key: string, value: unknown) {
  try {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  } catch (e) {
    // Android throws here when the 6 MB database is full.
    console.warn('storage write failed', key, e);
  }
}

Two details in there earn their keep:

  • Versioned key names. settings.v1 costs nothing today and saves you a migration script the first time the shape of that object changes. Bump the suffix, and old installs fall through to the fallback instead of parsing a stale shape.
  • A catch that deletes. A half-written or schema-drifted value should degrade to the default, not throw inside a render. Persistence is a cache; treat a bad read as a miss.

The hydration flash, and how to stop it

This is the number-one AsyncStorage bug and it is a rendering problem, not a storage one. Your state starts empty, React renders, then the promise resolves. Users see the login screen for 200 ms on every cold start, or the app flashes light before your saved dark theme lands.

// providers/SettingsProvider.tsx
const [settings, setSettings] = useState(DEFAULTS);
const [hydrated, setHydrated] = useState(false);

useEffect(() => {
  let alive = true;
  load(KEYS.settings, DEFAULTS).then((s) => {
    if (!alive) return;
    setSettings(s);
    setHydrated(true);          // flip AFTER the state is applied
  });
  return () => { alive = false; };
}, []);

// Keep the native splash up instead of rendering a wrong first frame:
if (!hydrated) return null;

return <SettingsContext.Provider value={{ settings, setSettings }}>{children}</SettingsContext.Provider>;

Returning null only works if something else is on screen — which is exactly what holding the splash screen with preventAutoHideAsync is for. Hide it in the same place you flip hydrated, and the user never sees a frame of the wrong state.

Batch writes — do not await in a loop

Each setItem is a round-trip across the bridge to native storage. Twelve of them in sequence on a settings save is twelve round-trips, and on a slow Android device that is a visible stall.

// Slow: one round-trip per key
for (const [k, v] of entries) await AsyncStorage.setItem(k, JSON.stringify(v));

// Fast: one batched native call
await AsyncStorage.multiSet(entries.map(([k, v]) => [k, JSON.stringify(v)]));

// Same on the way in
const pairs = await AsyncStorage.multiGet([KEYS.settings, KEYS.draft, KEYS.lastSync]);

One more pattern worth knowing: for a value that changes on every keystroke — a draft, a search box, a scroll position — debounce the write by 300–500 ms rather than persisting each character. And avoid AsyncStorage.clear()as a logout implementation; it wipes every library’s keys too, including your analytics install ID and any persisted query cache. Remove your own keys explicitly.

The 6 MB wall on Android

On Android, AsyncStorage is backed by a SQLite database with a default ceiling of 6 MB. Cross it and writes start failing — usually not on your phone, and usually months after launch when a cached list has grown. The ceiling is raiseable in a bare or prebuilt project:

# android/gradle.properties
AsyncStorage_db_size_in_MB=20

That requires a native rebuild, so it does not apply in Expo Go — and if you need it at all, that is a strong signal you are storing the wrong shape of data. A cache that grows without bound wants an eviction policy or a real database, not a bigger number in a properties file. iOS has no comparable documented cap, which is precisely why this bug is always Android-only and always a surprise.

Where each kind of data actually belongs

DataHomeWhy
Theme, units, onboarding seenAsyncStorageTiny, read once at launch, no privacy weight
Auth / refresh tokenexpo-secure-storeNeeds OS-level encryption at rest
Cached API responsesAsyncStorage or TanStack Query persisterFine until the payloads get big
Offline records the user editsexpo-sqliteNeeds queries, indexes, and partial writes
Values read on every renderreact-native-mmkvSynchronous, no await in render
Photos, video, PDFsexpo-file-systemBlobs do not belong in a KV store

The honest summary: AsyncStorage is correct for small, non-sensitive values read a handful of times per session. That covers more of a real app than people expect — and the moment it does not, the replacement is usually SQLite for shape reasons or SecureStore for privacy reasons, not MMKV for speed reasons.

Persisting a whole store

If you already use Zustand, do not hand-roll the effect — the persist middleware handles hydration, versioning, and partial persistence, and it exposes the hydration flag you need for the section above:

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

export const useSettings = create(
  persist(
    (set) => ({ theme: 'system', units: 'metric', setTheme: (theme) => set({ theme }) }),
    {
      name: 'settings.v1',
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (s) => ({ theme: s.theme, units: s.units }),  // never persist functions or transient UI
      version: 1,
      migrate: (persisted, from) => (from === 0 ? { ...persisted, units: 'metric' } : persisted),
    },
  ),
);

// Gate your first render on this:
const hydrated = useSettings.persist.hasHydrated();

partialize is the field people skip and regret: without it, transient UI state and loading flags get written to disk and restored on next launch, which is how an app comes back with a spinner that never resolves.

Skip the boilerplate

Storage wiring, hydration gating, and a splash that waits for it are the same twenty minutes in every project. Describe your app at shipnative.dev and it generates a React Native app with persistence already set up correctly — running on your phone in minutes, with the full Expo project available to export.

Frequently Asked Questions

Is AsyncStorage still part of React Native?

No. It was removed from React Native core and lives in the community package @react-native-async-storage/async-storage. Old tutorials that import AsyncStorage from react-native are outdated — that import has been gone for several major versions. In Expo, install it with npx expo install @react-native-async-storage/async-storage.

Is React Native local storage secure?

No. AsyncStorage is unencrypted — an SQLite file on Android and a plain file in your app container on iOS. Anyone with a rooted device, a filesystem dump, or an unencrypted device backup can read it. Preferences and cached content are fine there; auth tokens belong in expo-secure-store.

How much can AsyncStorage store?

On Android the default database ceiling is 6 MB, and it is raised with AsyncStorage_db_size_in_MB in gradle.properties (which requires a rebuild, so it is not available in Expo Go). On iOS there is no fixed documented cap and disk space is the practical limit. Either way, it is a key-value store, not a database — sizeable data sets belong in expo-sqlite.

Why does my app flash the logged-out screen before loading the saved session?

Because AsyncStorage reads are asynchronous while your first render is not. The component renders once with the initial (empty) state, then the value arrives a tick later. Fix it by holding a hydrated boolean, returning your splash or null until it flips true, and only then rendering the real tree.

Should I use AsyncStorage or MMKV?

AsyncStorage if you want zero friction, Expo Go support, and the widest library compatibility. react-native-mmkv if you are reading values during render or in hot paths — it is synchronous and much faster, at the cost of needing a development build because it ships native code. Most apps genuinely do not need the upgrade.

Does AsyncStorage data survive an app update?

Yes. It survives app updates and over-the-air updates, and is wiped on uninstall on both platforms. That last part differs from the iOS Keychain, which survives deletion — a useful asymmetry for detecting fresh installs.

→

Expo SecureStore

Where the token goes, and why not here.

Read guide →
→

Offline-First React Native Apps

Sync, conflict handling, and the storage layer underneath.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.