skip to content

What changed in React Native Async Storage v3 compared with v2, and why does the package still ship a default export?

level: middleimportance: should knowfreq 42%

answer

  1. no longer a singleton
  2. createAsyncStorage(name) per data set
  3. multi* renamed to *Many
  4. merge, callbacks and useAsyncStorage gone
  5. default export reads v2 data

basics

~20 s

Async Storage v3 replaces the global singleton with isolated instances from createAsyncStorage(name), drops callbacks, the merge API and useAsyncStorage, renames multiGet/multiSet/multiRemove to getMany/setMany/removeMany, and throws AsyncStorageError. The default export stays so existing v2 data remains readable during migration.

solid answer

~40 s

In v2 you imported one global `AsyncStorage` object. In v3 you call `createAsyncStorage('user')` and get an **isolated storage area** backed by its own database, so separate data sets cannot collide and `clear()` touches only that instance. The API is promise-only: callbacks are gone, the inconsistent `mergeItem`/`multiMerge` behaviour was removed, and the `useAsyncStorage` hook was dropped. Batch methods were renamed and reshaped: `getMany(keys)` returns a `Record<string, string | null>` and `setMany` takes a record. All failures are `AsyncStorageError` with a `type`. The default export survives as a **legacy singleton over the v2 data**, so an upgraded app can still read what users already stored and migrate at its own pace. v3 also needs React Native 0.76 or newer on iOS and Android.

code

typescript · 20 lines
typescript
import AsyncStorage, { createAsyncStorage } from "@react-native-async-storage/async-storage";

const preferences = createAsyncStorage("preferences");
const MARKER = "migratedFromV2";

export async function migrateLegacyPreferences(): Promise<void> {
  if ((await preferences.getItem(MARKER)) !== null) return;

  const wanted = ["onboardingDone", "language"];
  const legacy = await AsyncStorage.getMany(wanted);

  const entries: Record<string, string> = {};
  for (const [key, value] of Object.entries(legacy)) {
    if (value !== null) entries[key] = value;
  }
  entries[MARKER] = JSON.stringify(true);
  await preferences.setMany(entries);

  await AsyncStorage.removeMany(wanted);
}

go deeper

for a junior

Recall that v3 storage is created with createAsyncStorage(name) and that the old multiGet, multiSet and multiRemove are now getMany, setMany and removeMany.

for a middle

Explain scoped storages, what was removed (callbacks, merge, useAsyncStorage) and why the default export still points at v2 data for incremental migration.

for a senior

Show a rerunnable migration from the legacy storage into named instances, including when to delete legacy keys, and flag the platforms where scoped storages fall back.

for a principal

Weigh migrating eagerly at startup against lazily per key, considering launch time, users who skip versions and how long the legacy read path must stay in the app.

## From a singleton to scoped storages Version 2 of `@react-native-async-storage/async-storage` exposed a single global object: every screen and library that imported it shared one key space. Version 3 makes storage **instance-based**: ```typescript import { createAsyncStorage } from "@react-native-async-storage/async-storage"; const preferences = createAsyncStorage("preferences"); const cache = createAsyncStorage("cache"); ``` Each name is a **scoped storage**, an isolated storage area. On iOS, macOS and Android the name becomes its own SQLite database file under an `async-storage/databases/<name>/` directory; on the web it becomes an IndexedDB database of that name. Consequences: - Keys in `preferences` never collide with keys in `cache`. - `clear()` and `getAllKeys()` apply to one instance only. - The library's docs advise against file extensions in the name (for example `user.db`). Windows and visionOS cannot create scoped storages; there `createAsyncStorage` falls back, with a console warning in development builds, to the single legacy storage. ## What was removed or renamed | v2 | v3 instance | Note | |---|---|---| | `multiGet(keys, cb?)` returning `[key, value]` pairs | `getMany(keys)` returning `Record<string, string \| null>` | every requested key appears, `null` if absent | | `multiSet([[k, v], ...], cb?)` | `setMany({ k: v, ... })` | takes a record | | `multiRemove(keys, cb?)` | `removeMany(keys)` | missing keys ignored | | `mergeItem`, `multiMerge` | removed | merge semantics differed across platforms | | `useAsyncStorage(key)` hook | removed | the maintainers plan a redesigned one | | optional callback arguments | removed | promises only | | assorted thrown errors | `AsyncStorageError` with a `type` | predictable handling | The single-key methods `getItem`, `setItem`, `removeItem`, plus `getAllKeys` and `clear`, keep their v2 names and arguments (minus the callback), so simple call sites port with little more than a new receiver. ## Why the default export still exists Apps upgrading from v2 already have user data on disk in the old storage. If the upgrade made that data unreachable, every user would lose their settings on update. So the package's default export is a **singleton bound to the legacy v2 storage**: ```typescript import AsyncStorage from "@react-native-async-storage/async-storage"; await AsyncStorage.getItem("language"); // reads what v2 wrote ``` In the 3.1.1 source this object implements the same v3 `AsyncStorage` interface as an instance, so it offers `getMany`/`setMany`/`removeMany` rather than `multiGet`; old batch call sites still need renaming even if they keep the default import. The source marks this legacy storage as discouraged and "provided only as a migration path". New code should use named instances. ## Migrating existing data Because the two storages are separate, upgrading the package moves nothing by itself. A one-time migration at startup usually looks like this: 1. Read every legacy key with `getAllKeys()` and `getMany()` on the default export. 2. Write the entries you still need into the right named instance with `setMany()`. 3. Record a "migrated" marker in the new instance. 4. Only then remove the legacy keys with `removeMany()` or `clear()`. Writing the marker last makes the step safe to rerun if the app is killed half-way. ## Platform and build requirements - React Native **0.76+** on iOS and Android (higher minimums for macOS, visionOS and Windows). - Android `minSdk` 24 and Kotlin 2.1+; iOS 13+. - The native side is a Turbo Module shared across platforms and built on SQLite; on Android it uses Room. - 3.0 needed an extra Android build step; **3.1.0** publishes that artifact to Maven Central, removing the step. ## An upgrade checklist When a codebase moves from v2 to v3, the mechanical work is predictable: - Replace the global import at each call site with a named instance created once in a small storage module. - Rename `multiGet`/`multiSet`/`multiRemove` and adapt to the record shapes; code that indexed `result[0][1]` must now read `result[key]`. - Delete callback arguments and any `mergeItem` usage, replacing merges with read-modify-write. - Replace `useAsyncStorage` with your own small hook or a state library's persistence. - Update `catch` blocks to check `instanceof AsyncStorageError` and its `type`. - Rebuild the native app, since the native module changed. ## Why the changes were made The common thread is predictability. A global key space made collisions and accidental wipes easy; merge behaved differently per platform; callbacks and promises duplicated every method; and errors came in many shapes. Scoped instances, one calling convention and one error type trade a slightly longer setup line for storage you can reason about per feature.

  • Does upgrading to v3 move existing user data into your new instances?
    No. The default export keeps pointing at the legacy v2 storage and a named instance starts empty. You copy what you need yourself, typically once at startup, write a marker, and remove the legacy keys only after the copy succeeded.
  • Your v2 code called `AsyncStorage.mergeItem` to patch a settings object. What replaces it?
    Nothing in the library: merge was removed because it behaved differently across platforms. Read the value, parse it, change the field in JavaScript, and write the whole string back with `setItem`. If you patch often, that is a sign the data wants a store with partial updates.
  • What happens if you call `createAsyncStorage('prefs')` on Windows or visionOS?
    Scoped storages are not supported there, so the call returns the single legacy storage. Two different names therefore share one key space on those targets, which matters if you rely on `clear()` wiping only one instance.

saying these in an interview costs you the question

  • Still treating AsyncStorage as one global singleton in new v3 code
  • Expecting multiGet to keep working with the same array-of-pairs result
  • Assuming the package upgrade migrates v2 data into new instances automatically
  • Reaching for mergeItem or useAsyncStorage, both removed in v3
  • Believing the default export is the recommended API for new code