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

Redux Toolkit: Building a Favorites Feature in React Native

Redux Toolkit: Building a Favorites Feature

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

ProductCard's isFavorite heart from "React Native for Beginners" has so far been purely LOCAL – every card has its OWN, independent state, lost when leaving the screen. We'll make favorites GLOBAL and PERSISTENT, this time with Redux Toolkit instead of Zustand – directly comparing both approaches, just like "React for Professionals" chapter 29.

Achtung: As with "React for Professionals": in a real project you would normally NOT use Zustand AND Redux Toolkit at the same time. We deliberately build BOTH into this same app here (cart stays on Zustand, favorites use Redux Toolkit) so you can directly compare both approaches – a teaching device, not a production recommendation.

Installing Redux Toolkit and React-Redux

npx expo install @reduxjs/toolkit react-redux

Creating store/favoritesSlice.js

store/favoritesSlice.js
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;

We only store the sku values of favorited products, not the full product objects – a lean store that still gets the full product data from magentoApi, instead of duplicating it.

Creating store/index.js

store/index.js
import { configureStore } from '@reduxjs/toolkit';
import favoritesReducer from './favoritesSlice';

export const store = configureStore({
  reducer: {
    favorites: favoritesReducer,
  },
});

Achtung: Redux Toolkit's persist equivalent is the separate redux-persist library – deliberately OMITTED here to keep the store comparison focused (favorites are lost on app restart unless you add redux-persist yourself). Zustand's built-in persist middleware from chapter 2 has a genuine convenience advantage here – a legitimate point in the "which store do I pick" decision.

App.js: wiring up the Redux provider

App.js
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> WRAPS <NavigationContainer>, not the other way around – the order doesn't technically matter here (both are independent context providers), but this arrangement is the common convention: data providers (Redux) outside, UI providers (navigation) inside.

ProductCard.js: turning it into a dumb component

ProductCard loses its local isFavorite state entirely – just like in "React for Professionals" chapter 29, it becomes a pure display component that receives isFavorite and onToggleFavorite as props from outside, without knowing Redux exists at all:

components/ProductCard.js
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: wiring up favorites per card

// In screens/ProductListScreen.js:

// New imports:
import { useSelector, useDispatch } from 'react-redux';
import { toggleFavorite } from '../store/favoritesSlice';

// Inside the component, alongside the existing hooks:
const favoriteSkus = useSelector((state) => state.favorites.skus);
const dispatch = useDispatch();

// In the FlatList's renderItem, extend ProductCard with two more props:
<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) is unproblematic with FEW products (our 6 per page), but becomes an O(n) search PER list item with VERY many favorited items. For very large favorites lists, a Set or an object ({{ [sku]: true }}) in the store instead of an array would be more performant – not relevant for our small product catalog, but a good point for the performance chapters coming up next.

Creating screens/FavoritesScreen.js

screens/FavoritesScreen.js
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}>No favorites selected yet.</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}} is hardcoded – every product in FavoritesScreen is by definition already favorited. Tapping the heart removes the item from favoriteSkus, which makes favoriteProducts SMALLER on the next render – the card immediately disappears from the list, with no manual reload.

Tipp: Navigate to Favorites (e.g. via a test button in ProductListScreen's header, similar to the cart badge from chapter 1), favorite a product on the product list, switch to Favorites – it appears IMMEDIATELY, without FavoritesScreen needing to "know" anything about ProductListScreen. That's exactly the value of a global store: independent screens stay independent in CODE, but stay in sync in DATA.