Redux Toolkit: Favoriten-Feature in React Native bauen
Redux Toolkit: Ein Favoriten-Feature bauen
~18 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
ProductCards isFavorite-Herz aus "React Native für Einsteiger" ist bisher rein LOKAL – jede Karte hat ihren EIGENEN, unabhängigen Zustand, der beim Verlassen des Screens verloren geht. Wir machen Favoriten GLOBAL und DAUERHAFT, diesmal mit Redux Toolkit statt Zustand – im direkten Vergleich der beiden Ansätze, genau wie in "React für Profis" Kapitel 29.
Achtung: Wie schon bei "React für Profis": in einem echten Projekt würde man normalerweise NICHT Zustand UND Redux Toolkit gleichzeitig einsetzen. Wir bauen hier bewusst BEIDE in dieselbe App ein (Warenkorb bleibt bei Zustand, Favoriten nutzen Redux Toolkit), damit Sie beide Ansätze direkt vergleichen können – ein didaktischer Kunstgriff, keine Produktionsempfehlung.
Redux Toolkit und React-Redux installieren
npx expo install @reduxjs/toolkit react-reduxstore/favoritesSlice.js anlegen
import { createSlice } from '@reduxjs/toolkit';
const favoritesSlice = createSlice({
name: 'favorites',
initialState: { skus: [] },
reducers: {
toggleFavorite(state, action) {
const sku = action.payload;
if (state.skus.includes(sku)) {
state.skus = state.skus.filter((s) => s !== sku);
} else {
state.skus.push(sku);
}
},
},
});
export const { toggleFavorite } = favoritesSlice.actions;
export default favoritesSlice.reducer;Wir speichern nur die sku-Werte der favorisierten Produkte, nicht die kompletten Produktobjekte – ein schlanker Store, der die vollständigen Produktdaten weiterhin von magentoApi bezieht, statt sie zu duplizieren.
store/index.js anlegen
import { configureStore } from '@reduxjs/toolkit';
import favoritesReducer from './favoritesSlice';
export const store = configureStore({
reducer: {
favorites: favoritesReducer,
},
});Achtung: Redux Toolkits persist-Äquivalent ist die separate Bibliothek redux-persist – hier bewusst WEGGELASSEN, um den Store-Vergleich fokussiert zu halten (Favoriten gehen beim App-Neustart verloren, solange Sie redux-persist nicht selbst ergänzen). Zustands eingebaute persist-Middleware aus Kapitel 2 hat hier einen echten Komfort-Vorteil – ein legitimer Punkt für die "welchen Store wähle ich"-Entscheidung.
App.js: den Redux-Provider einbinden
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Provider } from 'react-redux';
import { store } from './store';
import ProductListScreen from './screens/ProductListScreen';
import ProductDetailScreen from './screens/ProductDetailScreen';
import FavoritesScreen from './screens/FavoritesScreen';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<Provider store={store}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="ProductList"
component={ProductListScreen}
options={{ title: 'Product Catalog' }}
/>
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={{ title: 'Product Details' }}
/>
<Stack.Screen
name="Favorites"
component={FavoritesScreen}
options={{ title: 'Favorites' }}
/>
</Stack.Navigator>
</NavigationContainer>
</Provider>
);
}<Provider> UMSCHLIESST <NavigationContainer>, nicht umgekehrt – die Reihenfolge ist hier egal (beide sind unabhängige Context-Provider), aber diese Anordnung ist die gängige Konvention: Daten-Provider (Redux) außen, UI-Provider (Navigation) innen.
ProductCard.js: zur Dumb Component machen
ProductCard verliert seinen lokalen isFavorite-State komplett – genau wie in "React für Profis" Kapitel 29 wird er zu einer reinen Anzeige-Komponente, die isFavorite und onToggleFavorite als Props von außen bekommt, ohne zu wissen, DASS es Redux überhaupt gibt:
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
function ProductCard({ name, price, imageUrl, onPress, isFavorite, onToggleFavorite }) {
return (
<TouchableOpacity style={styles.card} onPress={onPress}>
<Image source={{ uri: imageUrl }} style={styles.image} />
<View style={styles.info}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.price}>${price.toFixed(2)}</Text>
</View>
<TouchableOpacity style={styles.favoriteButton} onPress={onToggleFavorite}>
<Text style={styles.favoriteIcon}>{isFavorite ? '♥' : '♡'}</Text>
</TouchableOpacity>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
padding: 12,
marginBottom: 8,
backgroundColor: '#f9fafb',
borderRadius: 8,
alignItems: 'center',
},
image: { width: 60, height: 60, borderRadius: 4 },
info: { marginLeft: 12, flex: 1 },
name: { fontSize: 16, fontWeight: '600' },
price: { fontSize: 14, color: '#6b7280' },
favoriteButton: { padding: 8 },
favoriteIcon: { fontSize: 20, color: '#ef4444' },
});
export default ProductCard;ProductListScreen.js: Favoriten pro Karte anbinden
// In screens/ProductListScreen.js:
// Neue Imports:
import { useSelector, useDispatch } from 'react-redux';
import { toggleFavorite } from '../store/favoritesSlice';
// Innerhalb der Komponente, zusätzlich zu den bestehenden Hooks:
const favoriteSkus = useSelector((state) => state.favorites.skus);
const dispatch = useDispatch();
// Im renderItem der FlatList, ProductCard um zwei Props erweitern:
<ProductCard
name={item.name}
price={item.price}
imageUrl={item.imageUrl}
onPress={() => navigation.navigate('ProductDetail', { productSku: item.sku })}
isFavorite={favoriteSkus.includes(item.sku)}
onToggleFavorite={() => dispatch(toggleFavorite(item.sku))}
/>Achtung: favoriteSkus.includes(item.sku) ist bei WENIGEN Produkten (unsere 6 pro Seite) unproblematisch, wird aber bei SEHR vielen favorisierten Artikeln zu einer O(n)-Suche PRO Listeneintrag. Für sehr große Favoritenlisten wäre ein Set oder ein Objekt ({{ [sku]: true }}) im Store statt eines Arrays performanter – für unseren kleinen Produktkatalog nicht relevant, aber ein guter Punkt für die Performance-Kapitel, die als Nächstes folgen.
screens/FavoritesScreen.js anlegen
import { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { toggleFavorite } from '../store/favoritesSlice';
import { fetchProducts } from '../api/magentoApi';
import ProductCard from '../components/ProductCard';
function FavoritesScreen({ navigation }) {
const [allProducts, setAllProducts] = useState([]);
const [loading, setLoading] = useState(true);
const favoriteSkus = useSelector((state) => state.favorites.skus);
const dispatch = useDispatch();
useEffect(() => {
fetchProducts()
.then(setAllProducts)
.finally(() => setLoading(false));
}, []);
if (loading) {
return <ActivityIndicator size="large" style={styles.loader} />;
}
const favoriteProducts = allProducts.filter((product) =>
favoriteSkus.includes(product.sku)
);
return (
<View style={styles.container}>
<FlatList
data={favoriteProducts}
keyExtractor={(item) => item.sku}
renderItem={({ item }) => (
<ProductCard
name={item.name}
price={item.price}
imageUrl={item.imageUrl}
onPress={() =>
navigation.navigate('ProductDetail', { productSku: item.sku })
}
isFavorite={true}
onToggleFavorite={() => dispatch(toggleFavorite(item.sku))}
/>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>Noch keine Favoriten ausgewählt.</Text>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 16, paddingHorizontal: 16 },
loader: { flex: 1, justifyContent: 'center' },
emptyText: { textAlign: 'center', marginTop: 40, color: '#9ca3af' },
});
export default FavoritesScreen;isFavorite={{true}} fest verdrahtet – jedes Produkt in FavoritesScreen ist per Definition bereits favorisiert. Antippen des Herzens entfernt den Artikel aus favoriteSkus, wodurch favoriteProducts beim nächsten Render KLEINER wird – die Karte verschwindet dadurch sofort aus der Liste, ohne manuelles Neuladen.
Tipp: Navigieren Sie zu Favorites (z. B. testweise über einen Button in ProductListScreens Header, analog zum Warenkorb-Badge aus Kapitel 1), favorisieren Sie ein Produkt auf der Produktliste, wechseln Sie zu Favorites – es erscheint SOFORT, ohne dass FavoritesScreen irgendetwas von ProductListScreen "wissen" musste. Genau das ist der Wert eines globalen Stores: unabhängige Screens bleiben unabhängig im CODE, aber synchron in den DATEN.