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

Die Grenzen von useCart in React Native

Die Grenzen von useCart

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

Willkommen zurück! Ab hier setzen wir "React Native für Einsteiger" direkt fort – dieselbe produktkatalog-app, kein neues Projekt. Falls Sie diese Serie neu beginnen: Sie brauchen den Endstand der 18 Kapitel von "React Native für Einsteiger", um hier mitzumachen.

Startpunkt dieser Serie – Endstand von "React Native für Einsteiger"

produktkatalog-app/
├── App.js                        (Navigation)
├── app.json                      (Expo-Konfiguration)
├── package.json
├── api/
│   └── magentoApi.js             (Magento-REST-API: Produkte, Produktdetails)
├── components/
│   └── ProductCard.js            (wiederverwendbare Produktkarte)
├── hooks/
│   └── useCart.js                (Warenkorb, dauerhaft per AsyncStorage)
├── screens/
│   ├── ProductListScreen.js      (Produktliste + Suche)
│   └── ProductDetailScreen.js    (Produktdetails + Warenkorb-Button)
└── assets/                       (Icons, Bilder)

Was erwartet Sie in "React Native für Profis"?

Fünf Themenblöcke, aufeinander aufbauend, direkt am bestehenden Projekt: State-Management (Zustand, Redux Toolkit – die Grenzen von useCart, die wir in diesem Kapitel aufdecken, praktisch lösen), Performance (Profiling, FlatList-Optimierung, Hermes, Bilder-Performance), Internals (die neue Architektur: Fabric, TurboModules, JSI, Reanimated, Gesture Handler), TypeScript-Integration (das bestehende Projekt typisieren – die TypeScript-Sprache selbst ist Thema eines eigenen, separaten Tutorials) und Testing/Praxis (Jest, EAS Build, Best Practices, Interview-Vorbereitung).

Was useCart wirklich gut kann

Bevor wir über Grenzen sprechen: useCart aus "React Native für Einsteiger" Kapitel 12 war die RICHTIGE Lösung für sein Problem – Code-Duplikation vermeiden (AsyncStorage-Logik nicht in jedem Screen neu schreiben) und ein sauberes, wiederverwendbares Interface (cart, addProduct) bieten. Zur Erinnerung der Endstand aus jenem Kapitel:

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 };
}

Das eigentliche Problem: JEDER Aufruf erzeugt eine UNABHÄNGIGE Instanz

Hier wird es "Profi"-relevant: useCart() ist eine ganz normale Funktion, die intern useState aufruft. Ruft eine ZWEITE Komponente ebenfalls useCart() auf, bekommt sie ihren EIGENEN, komplett unabhängigen cart-State – KEINE geteilte Referenz, KEINE automatische Synchronisation. Beide Instanzen laden zwar aus DERSELBEN AsyncStorage-Datei, aber nur beim JEWEILIGEN eigenen Mount, nicht reaktiv bei Änderungen der anderen Instanz.

Das Problem sichtbar machen: ein Warenkorb-Badge

Wir fügen ProductListScreen ein Warenkorb-Badge im Header hinzu – eine zweite, UNABHÄNGIGE useCart()-Instanz neben der bereits in ProductDetailScreen vorhandenen:

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(); // zweite, UNABHÄNGIGE Instanz - siehe Erklärung unten

  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="Produkte suchen..."
        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}>Keine Produkte gefunden.</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 statt useEffect für navigation.setOptions – so aktualisiert sich der Header VOR dem sichtbaren Neuzeichnen, kein kurzes "Aufflackern" des alten Badge-Werts.

Den Bug reproduzieren

  1. App starten, auf der Produktliste ist das Badge "???? 0" sichtbar
  2. Ein Produkt antippen, um zu ProductDetailScreen zu navigieren
  3. "In den Warenkorb" antippen – ProductDetailScreens EIGENE useCart()-Instanz aktualisiert sich korrekt
  4. Mit dem Zurück-Pfeil zu ProductListScreen zurückkehren
  5. Das Badge zeigt WEITERHIN "???? 0" – obwohl der Artikel tatsächlich in AsyncStorage gespeichert wurde!

Der Grund: ProductListScreens useCart()-Instanz hat AsyncStorage nur EINMAL beim ALLERERSTEN Mount gelesen (leeres Array). Da React Navigation Screens standardmäßig NICHT unmountet, wenn man zu ihnen zurücknavigiert (sie bleiben im Stack "am Leben"), läuft der useEffect mit loadCart() kein zweites Mal – das Badge zeigt den STAND VON VOR DEM HINZUFÜGEN, dauerhaft veraltet, bis die App komplett neu gestartet wird.

Achtung: Das ist KEIN Bug in React Native selbst, sondern die logische Konsequenz aus useCart()s Architektur: ZWEI Komponenten, die useCart() aufrufen, haben ZWEI unabhängige "Wahrheiten" über den Warenkorb-Inhalt, die nur zufällig beim Start übereinstimmen. Genau dieses Muster – lokaler useState in einem Custom Hook, der von MEHREREN Stellen gleichzeitig genutzt wird – ist eine der häufigsten Ursachen für "meine Daten sind nicht synchron"-Bugs in React-Native-Apps.

Warum nicht einfach ein Context?

Man KÖNNTE useCart in einen React-Context-Provider verwandeln (siehe "React für Profis" Kapitel 27 für die ausführliche Grenzen-Diskussion von Context) – das würde das Synchronisationsproblem lösen, aber ein NEUES einführen: JEDE Komponente, die useCart() aufruft, würde bei JEDER noch so kleinen Warenkorb-Änderung neu rendern, selbst wenn sie nur an EINEM einzelnen Wert interessiert ist. Die Lösung, die wir im nächsten Kapitel bauen – Zustand – löst BEIDE Probleme gleichzeitig: echte Synchronisation UND gezielte, selektive Abonnements.

Tipp: Merksatz für den Rest dieser Serie: ein Custom Hook mit eigenem useState ist PERFEKT für Logik, die pro Komponente unabhängig sein SOLL (z. B. useDocumentTitle aus "React für Einsteiger", jede Komponente hat ihren eigenen Titel). Für Daten, die GLOBAL, GETEILT und SYNCHRON über die gesamte App hinweg sein müssen (Warenkorb, Login-Status), braucht es einen ECHTEN Store außerhalb des Komponentenbaums – genau das Thema des nächsten Kapitels.