Local Storage: AsyncStorage

Web apps have localStorage built right into the browser — a simple, synchronous key-value store. React Native has no built-in equivalent. Instead, the standard solution is a community-maintained package, @react-native-async-storage/async-storage, which gives you a very similar key-value API — with one crucial difference covered below: every single operation is asynchronous.

Installing AsyncStorage

AsyncStorage isn't part of core React Native — it's installed separately, like most native capabilities:

npm install @react-native-async-storage/async-storage

Then import it wherever you need it:

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

Everything Is Asynchronous

This is the single most important thing to internalize about AsyncStorage: unlike the web's localStorage.getItem(key), which returns a value immediately, every AsyncStorage method returns a Promise. You always need async/await (covered in the previous lesson, Async/Await & Error Handling) or .then() to actually get the value out.

Storing and Reading a String Value

Writing a value is done with setItem, which takes a key and a string:

async function saveUsername() {
  await AsyncStorage.setItem('username', 'alice');
}

Reading it back uses getItem, which resolves with the stored string:

async function loadUsername() {
  const value = await AsyncStorage.getItem('username');
  console.log(value); // 'alice'
}
Important detail: If the key was never set (or was removed), getItem resolves with null — not undefined. Always check with if (value !== null) or a similar explicit check, rather than assuming a falsy check catches every "not set" case the way you might with plain JavaScript object properties.

Removing a stored value is a single call to removeItem:

await AsyncStorage.removeItem('username');

Storing a Whole Object: JSON.stringify and JSON.parse

AsyncStorage only stores strings — there's no method that accepts a plain object directly. To store anything more complex than a single string, serialize it with JSON.stringify() before saving, and parse it back with JSON.parse() after reading:

async function savePreferences(preferences) {
  await AsyncStorage.setItem('userPreferences', JSON.stringify(preferences));
}

async function loadPreferences() {
  const raw = await AsyncStorage.getItem('userPreferences');
  return raw !== null ? JSON.parse(raw) : null;
}

// usage
await savePreferences({ theme: 'dark', fontSize: 16, notificationsEnabled: true });

const preferences = await loadPreferences();
console.log(preferences?.theme); // 'dark'

A Full Worked Example: A "Remember Me" Login Flow

A very common real-world use of AsyncStorage: saving an email address after a successful login, then pre-filling it automatically the next time the app opens, using useEffect to load the saved value when the component mounts:

import React, { useState, useEffect } from 'react';
import { View, TextInput, Button } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

function LoginScreen() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // pre-fill the saved email on the next app launch
  useEffect(() => {
    async function loadSavedEmail() {
      const savedEmail = await AsyncStorage.getItem('rememberedEmail');
      if (savedEmail !== null) {
        setEmail(savedEmail);
      }
    }
    loadSavedEmail();
  }, []);

  async function handleLogin() {
    const success = await performLogin(email, password); // your own auth call
    if (success) {
      await AsyncStorage.setItem('rememberedEmail', email);
    }
  }

  return (
    <View>
      <TextInput value={email} onChangeText={setEmail} placeholder="Email" />
      <TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
      <Button title="Log In" onPress={handleLogin} />
    </View>
  );
}

The next time the app is opened, useEffect runs on mount, finds the previously saved email, and calls setEmail — so the field is already filled in before the user touches anything.

When AsyncStorage Isn't the Right Tool

AsyncStorage is designed for small amounts of simple key-value data — settings, flags, small cached objects. It's not meant to hold large datasets, and it has no query language: there's no way to ask it "give me all items where price is under 20" the way a real database can. If your app needs to store and query a genuinely large or structured dataset — hundreds of records, relationships between them, filtering and sorting — reach for a proper embedded database instead, such as react-native-sqlite-storage (a real SQL database on-device) or WatermelonDB (a reactive database built specifically for React Native apps with large datasets). Both are outside the scope of this lesson, but worth knowing exist once AsyncStorage starts to feel like the wrong shape for your data.

Security Note: AsyncStorage Is Not Encrypted

Important: AsyncStorage stores data as plain, unencrypted files on the device by default. It's perfectly fine for non-sensitive data — a remembered email, a theme preference, a cached list of articles. It is not the right place to store passwords, authentication tokens, or any other sensitive credential. For that, use a package designed for secure storage, such as react-native-keychain (uses the OS-level Keychain on iOS and Keystore on Android) or, if you're using Expo, expo-secure-store. Both encrypt data at rest using the operating system's own secure storage mechanisms — something plain AsyncStorage was never designed to do.
Next up: With persistence covered, the next lesson moves on to visuals: Images & Assets — displaying local and network images correctly.