Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

The Limits of useCart in React Native

The Limits of useCart

~13 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Welcome back! From here on, we continue directly from "React Native for Beginners" – the same produktkatalog-app, not a new project. If you're starting this series fresh: you need the final state of the 18 chapters from "React Native for Beginners" to follow along here.

Starting point of this series – the final state of "React Native for Beginners"

produktkatalog-app/
├── App.js                        (navigation)
├── app.json                      (Expo configuration)
├── package.json
├── api/
│   └── magentoApi.js             (Magento REST API: products, product details)
├── components/
│   └── ProductCard.js            (reusable product card)
├── hooks/
│   └── useCart.js                (cart, persisted via AsyncStorage)
├── screens/
│   ├── ProductListScreen.js      (product list + search)
│   └── ProductDetailScreen.js    (product details + cart button)
└── assets/                       (icons, images)

What to expect in "React Native for Professionals"?

Five building blocks, each building on the last, applied directly to the existing project: state management (Zustand, Redux Toolkit – solving in practice the limits of useCart this chapter uncovers), performance (profiling, FlatList optimization, Hermes, image performance), internals (the New Architecture: Fabric, TurboModules, JSI, Reanimated, Gesture Handler), TypeScript integration (typing the existing project – the TypeScript language itself is the subject of its own, separate tutorial), and testing/practice (Jest, EAS Build, best practices, interview prep).

What useCart is genuinely good at

Before talking about limits: useCart from "React Native for Beginners" chapter 12 was the RIGHT solution for its problem – avoiding code duplication (not rewriting AsyncStorage logic in every screen) and offering a clean, reusable interface (cart, addProduct). As a reminder, its final state from that chapter:

hooks/useCart.js
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([]);

  useEffect(() => {
    async function loadCart() {
      const saved = await AsyncStorage.getItem(STORAGE_KEY);
      if (saved) {
        setCart(JSON.parse(saved));
      }
    }
    loadCart();
  }, []);

  async function addProduct(product) {
    const newCart = [...cart, product];
    setCart(newCart);
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newCart));
  }

  return { cart, addProduct };
}

The real problem: EVERY call creates an INDEPENDENT instance

Here's where it gets "professional"-relevant: useCart() is a perfectly normal function that internally calls useState. If a SECOND component also calls useCart(), it gets its OWN, completely independent cart state – NO shared reference, NO automatic synchronization. Both instances load from the SAME AsyncStorage file, but only at their OWN respective mount, not reactively when the other instance changes.

Making the problem visible: a cart badge

We'll add a cart badge to ProductListScreen's header – a second, INDEPENDENT useCart() instance alongside the one already in ProductDetailScreen:

screens/ProductListScreen.js
import { useState, useEffect, useLayoutEffect } from 'react';
import {
  View,
  Text,
  FlatList,
  TextInput,
  StyleSheet,
  ActivityIndicator,
} from 'react-native';
import { fetchProducts } from '../api/magentoApi';
import { useCart } from '../hooks/useCart';
import ProductCard from '../components/ProductCard';

function ProductListScreen({ navigation }) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [searchText, setSearchText] = useState('');
  const { cart } = useCart(); // second, INDEPENDENT instance - see explanation below

  useEffect(() => {
    async function loadProducts() {
      try {
        const items = await fetchProducts();
        setProducts(items);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    }
    loadProducts();
  }, []);

  useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <Text style={styles.cartBadge}>???? {cart.length}</Text>
      ),
    });
  }, [navigation, cart]);

  if (loading) {
    return <ActivityIndicator size="large" style={styles.loader} />;
  }

  const filteredProducts = products.filter((product) =>
    product.name.toLowerCase().includes(searchText.toLowerCase())
  );

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.searchInput}
        placeholder="Search products..."
        value={searchText}
        onChangeText={setSearchText}
      />
      <FlatList
        data={filteredProducts}
        keyExtractor={(item) => item.sku}
        renderItem={({ item }) => (
          <ProductCard
            name={item.name}
            price={item.price}
            imageUrl={item.imageUrl}
            onPress={() =>
              navigation.navigate('ProductDetail', { productSku: item.sku })
            }
          />
        )}
        ListEmptyComponent={<Text style={styles.emptyText}>No products found.</Text>}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, paddingTop: 16, paddingHorizontal: 16 },
  loader: { flex: 1, justifyContent: 'center' },
  cartBadge: { marginRight: 16, fontSize: 16 },
  searchInput: {
    borderWidth: 1,
    borderColor: '#d1d5db',
    borderRadius: 8,
    padding: 10,
    marginBottom: 12,
  },
  emptyText: { textAlign: 'center', marginTop: 40, color: '#9ca3af' },
});

export default ProductListScreen;

useLayoutEffect instead of useEffect for navigation.setOptions – this way the header updates BEFORE the visible repaint, no brief "flash" of the old badge value.

Reproducing the bug

  1. Start the app, on the product list the badge shows "???? 0"
  2. Tap a product to navigate to ProductDetailScreen
  3. Tap "Add to Cart" – ProductDetailScreen's OWN useCart() instance updates correctly
  4. Navigate back to ProductListScreen with the back arrow
  5. The badge STILL shows "???? 0" – even though the item was actually saved to AsyncStorage!

The reason: ProductListScreen's useCart() instance only read AsyncStorage ONCE, on the VERY FIRST mount (an empty array). Since React Navigation does NOT unmount screens by default when you navigate back to them (they stay "alive" in the stack), the useEffect with loadCart() doesn't run a second time – the badge shows the state FROM BEFORE THE ADDITION, permanently stale, until the app is fully restarted.

Achtung: This is NOT a bug in React Native itself, it's the logical consequence of useCart()'s architecture: TWO components calling useCart() have TWO independent "truths" about the cart's contents, which only happen to match at startup. Exactly this pattern – local useState inside a custom hook used from MULTIPLE places at once – is one of the most common causes of "my data isn't in sync" bugs in React Native apps.

Why not just a Context?

You COULD turn useCart into a React Context provider (see "React for Professionals" chapter 27 for the full limits discussion of Context) – that would fix the synchronization problem, but introduce a NEW one: EVERY component calling useCart() would re-render on EVERY tiny cart change, even if it's only interested in ONE single value. The solution we'll build next chapter – Zustand – solves BOTH problems at once: genuine synchronization AND targeted, selective subscriptions.

Tipp: A rule of thumb for the rest of this series: a custom hook with its own useState is PERFECT for logic that SHOULD be independent per component (e.g. useDocumentTitle from the web "React for Beginners" series, where each component legitimately sets its own title). For data that needs to be GLOBAL, SHARED, and SYNCHRONIZED across the whole app (cart, login status), you need a REAL store outside the component tree – exactly the topic of the next chapter.