Zustand: Managing the Cart Globally in React Native
Zustand: Managing the Cart Globally
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Zustand solves BOTH problems from the last chapter at once: a store exists OUTSIDE the component tree (genuine synchronization, no more duplicate state) AND components can subscribe to only the values they actually need via selectors (no unnecessary re-rendering).
Installing Zustand
npx expo install zustandCreating store/cartStore.js: with persist middleware
Unlike useCart, which manually calls AsyncStorage in every function, we'll use Zustand's built-in persist middleware – it handles loading/saving automatically in the background:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useCartStore = create(
persist(
(set, get) => ({
cart: [],
addProduct(product) {
set({ cart: [...get().cart, product] });
},
removeProduct(sku) {
set({ cart: get().cart.filter((item) => item.sku !== sku) });
},
}),
{
name: 'cart-storage', // key AsyncStorage stores the data under
storage: createJSONStorage(() => AsyncStorage),
}
)
);get() (alongside set, the second argument of the store creator) reads the CURRENT store state from INSIDE a store function – get().cart instead of a captured, potentially stale cart variable from a closure. persist(...) wraps the store definition and automatically handles loading on startup AND saving on EVERY set() change – no more manual AsyncStorage.getItem/setItem needed.
Achtung: createJSONStorage(() => AsyncStorage) is RN-specific – Zustand's persist middleware defaults to localStorage on the web (synchronous); in React Native it needs an ASYNCHRONOUS storage engine like AsyncStorage, explicitly specified via createJSONStorage. This exact adjustment was missing in "React for Professionals" chapter 28 (web), since it wasn't needed there.
Switching ProductDetailScreen.js to useCartStore
import { useState, useEffect } from 'react';
import {
View,
Text,
Image,
ActivityIndicator,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { fetchProductBySku } from '../api/magentoApi';
import { useCartStore } from '../store/cartStore';
function ProductDetailScreen({ route }) {
const { productSku } = route.params;
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const addProduct = useCartStore((state) => state.addProduct);
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.button}
onPress={() => addProduct(product)}
>
<Text style={styles.buttonText}>Add to Cart</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
loader: { flex: 1, justifyContent: 'center' },
error: { padding: 16, color: '#dc2626' },
image: { width: '100%', height: 240, borderRadius: 12, marginBottom: 16 },
name: { fontSize: 22, fontWeight: 'bold' },
price: { fontSize: 18, color: '#2563eb', marginVertical: 8 },
button: { backgroundColor: '#2563eb', padding: 14, borderRadius: 8, alignItems: 'center', marginTop: 16 },
buttonText: { color: 'white', fontWeight: 'bold' },
});
export default ProductDetailScreen;Switching ProductListScreen.js to useCartStore
// In screens/ProductListScreen.js, two changes:
// 1. Replace the import:
import { useCartStore } from '../store/cartStore';
// 2. Inside the component, instead of "const { cart } = useCart();":
const cart = useCartStore((state) => state.cart);useCartStore((state) => state.cart) instead of the full destructure – ProductListScreen only needs cart to display its length, not addProduct. The rest of the file (search, FlatList, header badge) stays UNCHANGED as shown in chapter 1.
Re-testing the bug from chapter 1
Repeat the steps from chapter 1: open a product, tap "Add to Cart", navigate back. The badge now IMMEDIATELY shows the correct, updated value – BOTH screens read the same, single cart state from the same store, no more independent copies.
Removing hooks/useCart.js
hooks/useCart.js is no longer imported by any component – delete the file AND the (now empty) hooks/ folder. The store now lives entirely in store/cartStore.js.
Tipp: createJSONStorage accepts ANY storage engine with getItem/setItem/removeItem methods – for sensitive data (see the password manager topic in the "React Native Reference" series), you could just as easily use expo-secure-store instead of AsyncStorage, with an identical persist configuration.