Storing Data Locally with AsyncStorage
Storing Data Locally with AsyncStorage
~8 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
So far, when a user closes and reopens the app, all state is gone – the cart is empty, favorites are forgotten. To persist data across app restarts, there's AsyncStorage.
Installation
npx expo install @react-native-async-storage/async-storageThe comparison to localStorage
If you know localStorage from the browser, AsyncStorage is conceptually almost identical – a key-value store for strings. The big difference: localStorage is SYNCHRONOUS (localStorage.setItem(...) blocks briefly until done), AsyncStorage is ASYNCHRONOUS (returns a promise you await with await) – necessary because native storage access on iOS/Android must not block, or the entire app UI would briefly freeze.
| HTML/Web | React Native |
|---|---|
localStorage.setItem('key', 'value') | await AsyncStorage.setItem('key', 'value') |
localStorage.getItem('key') | await AsyncStorage.getItem('key') |
localStorage.removeItem('key') | await AsyncStorage.removeItem('key') |
strings only, complex data via JSON.stringify() | identical: strings only, complex data via JSON.stringify()/JSON.parse() |
Creating hooks/useCart.js: a custom hook
Create a new folder hooks/ with the file useCart.js inside it. This is what's called a "custom hook" (function name starts with use) – a reusable function that combines other hooks like useState/useEffect:
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = 'cart';
export function useCart() {
const [cart, setCart] = useState([]);
// On startup: load the saved cart
useEffect(() => {
async function loadCart() {
const saved = await AsyncStorage.getItem(STORAGE_KEY);
if (saved) {
setCart(JSON.parse(saved));
}
}
loadCart();
}, []);
// On every change: save the cart
async function addProduct(product) {
const newCart = [...cart, product];
setCart(newCart);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newCart));
}
return { cart, addProduct };
}Any component that calls useCart() gets access to the same pattern of code without duplicating it. [...cart, product] creates a NEW array with all the old entries plus the new one – React state must never be mutated directly (e.g. cart.push(product) would be wrong), only ever replaced with a brand new object/array. This is one of the most common beginner mistakes.
Extending ProductDetailScreen.js with a cart button
Now let's use useCart in our real screens/ProductDetailScreen.js from chapter 10. Replace the entire content of the file with this version:
import { useState, useEffect } from 'react';
import {
View,
Text,
Image,
ActivityIndicator,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { fetchProductBySku } from '../api/magentoApi';
import { useCart } from '../hooks/useCart';
function ProductDetailScreen({ route }) {
const { productSku } = route.params;
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const { cart, addProduct } = useCart();
useEffect(() => {
async function loadProduct() {
try {
const data = await fetchProductBySku(productSku);
setProduct(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
loadProduct();
}, [productSku]);
if (loading) {
return <ActivityIndicator size="large" style={styles.loader} />;
}
if (!product) {
return <Text style={styles.error}>Could not load product.</Text>;
}
return (
<View style={styles.container}>
<Image source={{ uri: product.imageUrl }} style={styles.image} />
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.price}>${product.price.toFixed(2)}</Text>
<TouchableOpacity style={styles.cartButton} onPress={() => addProduct(product)}>
<Text style={styles.cartButtonText}>
Add to Cart ({cart.length})
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
loader: { flex: 1, justifyContent: 'center' },
error: { textAlign: 'center', marginTop: 24, color: '#ef4444' },
image: { width: '100%', height: 220, borderRadius: 8, marginBottom: 16 },
name: { fontSize: 22, fontWeight: 'bold', marginBottom: 8 },
price: { fontSize: 18, color: '#6b7280', marginBottom: 20 },
cartButton: {
backgroundColor: '#111827',
borderRadius: 8,
paddingVertical: 14,
alignItems: 'center',
},
cartButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
export default ProductDetailScreen;What's new is the useCart import, the line const {{ cart, addProduct }} = useCart();, and the button at the end. Since useCart loads and saves the cart itself via AsyncStorage, ProductDetailScreen doesn't need to worry about anything else – the cart survives even after fully closing the app.
Finished project structure – exactly what we announced in chapter 3
produktkatalog-app/ ├── App.js (entry point: navigation) ├── app.json (Expo configuration) ├── package.json (packages, like composer.json) ├── api/ │ └── magentoApi.js (connection to the Magento REST API) ├── components/ │ └── ProductCard.js (reusable product card) ├── hooks/ │ └── useCart.js (cart, persisted on the device) ├── screens/ │ ├── ProductListScreen.js (product list + search) │ └── ProductDetailScreen.js (product details + cart) └── assets/ (icons, images)
That completes our project: a real, working app with a product list, live search, a detail page, favorites, and a persistently saved cart – exactly the structure we set out to build in chapter 3. The remaining chapters now turn to debugging, testing on real devices, and publishing.
Achtung: AsyncStorage is NOT encrypted and not meant for sensitive data like passwords or payment information. For that, use expo-secure-store, which relies on the encrypted system keystores of iOS (Keychain) and Android (Keystore).