skip to content

A React Native to-do app reads its list from Async Storage at startup; how do you move that data into react-native-mmkv so later launches read synchronously without losing anything?

level: seniorimportance: should knowfreq 40%

answer

  1. one async launch, then sync forever
  2. marker read synchronously at module load
  3. copy strings as strings
  4. marker last, old keys deleted after
  5. InteractionManager is gone in 0.87

basics

~20 s

Check a migration flag in MMKV synchronously at startup. If it is missing, show a brief loading state, copy every Async Storage value into MMKV unchanged, set the flag, then delete the old keys. Later launches read synchronously.

solid answer

~40 s

The migration costs one asynchronous launch; after it, reads are synchronous. At module load I read `storage.getBoolean('hasMigratedFromAsyncStorage')`. If it is set, the to-do screen initialises its state with `useState(() => JSON.parse(storage.getString('todos') ?? '[]'))` and renders immediately. If not, the app shows a short loading view and runs the migration in an effect: `getAllKeys()` and `getMany()` on Async Storage (the package's default export if the data was written by v2), then `storage.set(key, value)` for each non-null value, keeping strings as strings so existing `getString` reads keep working. Only after every write succeeds do I set the flag and then remove the Async Storage keys. If the migration throws, the flag stays unset and the next launch retries.

code

tsx · 18 lines
tsx
import { useEffect, useState } from "react";
import { ActivityIndicator } from "react-native";
import { hasMigrated, migrateFromAsyncStorage } from "./storage";
import { TodoApp } from "./TodoApp";

export default function App() {
  const [ready, setReady] = useState(hasMigrated);

  useEffect(() => {
    if (hasMigrated) return;
    migrateFromAsyncStorage()
      .catch((e) => console.error("MMKV migration failed; will retry next launch", e))
      .finally(() => setReady(true));
  }, []);

  if (!ready) return <ActivityIndicator />;
  return <TodoApp />;
}

go deeper

for a junior

Recall that data already in Async Storage must be copied into MMKV once, and that only after that can reads be synchronous.

for a middle

Explain the flag-gated flow: synchronous flag check, loading state only on the migrating launch, getAllKeys plus getMany, then writes.

for a senior

Order the steps for crash safety (writes, flag, deletion), keep value types intact, pick the right Async Storage source, and define behaviour when the migration throws.

for a principal

Plan the rollout across releases: how long migration code stays, how failures are observed in the field, and when the Async Storage dependency can be dropped.

## Why migrate at all On Async Storage every read returns a Promise, so a to-do app cannot show the saved list in its first render: it renders a spinner or an empty list, then fills in. **react-native-mmkv** reads synchronously, so after the move the list is available during the first render. The data already on users' devices must come along, and the move itself is necessarily asynchronous because the source is. ## The shape of the migration The library ships a migration guide; the essentials are: 1. **Gate on a flag stored in MMKV.** Read `storage.getBoolean("hasMigratedFromAsyncStorage")` synchronously at module load. `true` means the app can take the synchronous path immediately. 2. **Render a loading state only when the flag is missing.** That happens once per install, on the first launch after the update. 3. **Read everything once.** `getAllKeys()` then one `getMany(keys)` rather than a `getItem` per key. 4. **Write each non-null value to MMKV.** 5. **Set the flag after all writes.** 6. **Delete the Async Storage data** with `removeMany(keys)` or `clear()`. 7. **Remove the migration code** in a later release, once installs that predate it are no longer a concern. ## Which Async Storage do you read? With Async Storage v3 the source depends on where the data was written: - Data written by **v2** lives in the legacy storage behind the package's **default export**. - Data written by v3 lives in a named instance, `createAsyncStorage("name")`. Read from the one your app actually wrote to; an app that upgraded Async Storage midway may need both. ## Traps that lose or corrupt data | Trap | What happens | Instead | |---|---|---| | Deleting old keys before the MMKV writes | a crash mid-migration loses data | delete last | | Setting the flag first | an interrupted run is never retried | set it after the writes | | Converting `"true"`/`"false"` strings to booleans for every key | a text field that happens to hold `"true"` no longer reads back as that string with `getString` | keep strings unless you know the key's type | | Swallowing an error and setting the flag anyway | partial data is treated as complete | leave the flag unset so the next launch retries | | Running it inside `InteractionManager.runAfterInteractions`, as older guides do | `InteractionManager` was removed in React Native 0.87 | start it from an effect, or `requestIdleCallback` if it must wait | The type trap deserves emphasis. MMKV getters are typed: a value written with a boolean must be read with `getBoolean`. Converting only the keys whose types you know, such as a `showCompleted` flag, is safe; a blanket conversion is not. ## A minimal implementation ```typescript import AsyncStorage from "@react-native-async-storage/async-storage"; import { createMMKV } from "react-native-mmkv"; export const storage = createMMKV(); const FLAG = "hasMigratedFromAsyncStorage"; export const hasMigrated = storage.getBoolean(FLAG) === true; export async function migrateFromAsyncStorage(): Promise<void> { const keys = await AsyncStorage.getAllKeys(); const values = await AsyncStorage.getMany(keys); for (const [key, value] of Object.entries(values)) { if (value !== null) storage.set(key, value); } storage.set(FLAG, true); await AsyncStorage.removeMany(keys); } ``` If the app is killed after the MMKV writes but before the flag, the next launch copies the same values again, which is harmless. If it is killed after the flag but before the deletion, the old keys linger unused; a later cleanup can clear them. ## Operational judgement - **Measure on a low-end Android device** with a realistic amount of data; the migration is a one-off cost, but a very slow first launch after an update still reads as a bug. - **Decide what failure means.** If the migration throws, the app can keep reading Async Storage for that session and retry on the next launch; it should not start with an empty to-do list. - **Log counts** of keys read and written so partial failures in the field are visible. - **Plan the removal.** The migration code and the Async Storage dependency can go once installs that predate the migration are negligible; until then the flag check costs one synchronous read per launch. - **Keep the list small.** Storing the whole to-do list as one JSON string still means rewriting it on every change; that is fine for a personal list, not for thousands of items.

  • Why not convert every stored 'true' or 'false' string into an MMKV boolean during migration?
    Because MMKV getters are typed. A text field whose content happens to be "true" would be written as a boolean, and code that reads it with `getString` would no longer get that string back. Convert only keys whose type you know, and copy the rest as strings.
  • In the sample App component, the migration fails and the app still renders TodoApp. Is that safe?
    Only if TodoApp can cope with MMKV lacking some data. The flag stays unset, so the next launch retries, but this session may show a partial list. A stricter option is to keep reading Async Storage as a fallback until the migration has succeeded once.

saying these in an interview costs you the question

  • Deleting Async Storage keys before MMKV writes have succeeded
  • Setting the migrated flag before copying the data
  • Blanket-converting 'true'/'false' strings into MMKV booleans
  • Scheduling the migration with InteractionManager on React Native 0.87
  • Expecting reads to be synchronous during the migrating launch too