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

Navigating Between Screens with React Navigation

Navigation Between Screens

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

So far our app has had just a single screen. A real product catalog needs at least an overview list and a detail page. On the web you'd use different URLs and <a href> links for that. React Native has no built-in system for this – we install the standard library React Navigation.

Installation

In your project folder (using the terminal in VS Code or WebStorm):

npx expo install @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context

expo install instead of npm install matters: Expo automatically checks which versions match your current Expo SDK version – comparable to composer require, which automatically picks versions compatible with your composer.json constraints.

App.js is getting messy – time to split it up

Our App.js now contains both data loading AND display in a single file. Right now, when we need a second screen, is exactly the right moment to move the current content out into its own file screens/ProductListScreen.js and make App.js lean again – responsible only for navigation.

Extending magentoApi.js: loading a single product

The detail screen needs a way to load ONE product by its SKU. Extend api/magentoApi.js with a second exported function (the existing lines stay unchanged, only the new function is added):

api/magentoApi.js
const BASE_URL = 'https://mironsoft.test/rest/en/V1';

function mapMagentoProduct(item) {
  const imageAttribute = item.custom_attributes?.find(
    (attribute) => attribute.attribute_code === 'image'
  );

  return {
    sku: item.sku,
    name: item.name,
    price: item.price,
    imageUrl: imageAttribute
      ? `https://mironsoft.test/media/catalog/product${imageAttribute.value}`
      : 'https://picsum.photos/200',
  };
}

export async function fetchProducts() {
  const response = await fetch(`${BASE_URL}/products?searchCriteria[pageSize]=20`);

  if (!response.ok) {
    throw new Error(`Magento API error: ${response.status}`);
  }

  const data = await response.json();
  return data.items.map(mapMagentoProduct);
}

export async function fetchProductBySku(sku) {
  const response = await fetch(`${BASE_URL}/products/${encodeURIComponent(sku)}`);

  if (!response.ok) {
    throw new Error(`Magento API error: ${response.status}`);
  }

  const item = await response.json();
  return mapMagentoProduct(item);
}

Magento identifies products through its REST API by SKU by default, not by numeric ID – that's why our identifier is called sku everywhere, not id. encodeURIComponent() matters in case a SKU contains special characters like spaces or slashes.

Tipp: Using option B (dummyjson.com) from chapter 8? Add the exact same fetchProductBySku() function – dummyjson.com/products/{{id}} works just like Magento's single-product endpoint, only BASE_URL and mapMagentoProduct() stay the ones from chapter 8.

Creating screens/ProductListScreen.js

Create a new folder screens/ with the file ProductListScreen.js inside it. The content is almost identical to what was just in App.js – only the function name changes, and onPress now navigates instead of just logging:

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

function ProductListScreen({ navigation }) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

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

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

  return (
    <View style={styles.container}>
      <FlatList
        data={products}
        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>No products found.</Text>}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, paddingTop: 16, paddingHorizontal: 16 },
  loader: { flex: 1, justifyContent: 'center' },
});

export default ProductListScreen;

Note the imports: '../api/magentoApi' and '../components/ProductCard' instead of './...' – since this file now lives one level deeper, inside screens/, it needs ../ to go up one folder first to reach api/ and components/.

Creating screens/ProductDetailScreen.js: our second screen

This is a brand new file – our first real second screen. It loads its own product based on the SKU passed via navigation:

screens/ProductDetailScreen.js
import { useState, useEffect } from 'react';
import { View, Text, Image, ActivityIndicator, StyleSheet } from 'react-native';
import { fetchProductBySku } from '../api/magentoApi';

function ProductDetailScreen({ route }) {
  const { productSku } = route.params;
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);

  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>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  loader: { flex: 1, justifyContent: 'center' },
  error: { textAlign: 'center', marginTop: 24, color: '#ef4444' },
  image: { width: '100%', height: 220, borderRadius: 8, marginBottom: 16 },
  name: { fontSize: 22, fontWeight: 'bold', marginBottom: 8 },
  price: { fontSize: 18, color: '#6b7280' },
});

export default ProductDetailScreen;

route.params contains exactly the object that ProductListScreen passed to navigation.navigate(...) – here {{ productSku }}. [productSku] as the second useEffect argument (instead of an empty array) makes sure it re-loads when navigating to a DIFFERENT product.

Cleaning up App.js: navigation only

Now let's replace App.js one last time – from now on it contains no data logic at all, only the navigation between our two screens:

App.js
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import ProductListScreen from './screens/ProductListScreen';
import ProductDetailScreen from './screens/ProductDetailScreen';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="ProductList"
          component={ProductListScreen}
          options={{ title: 'Product Catalog' }}
        />
        <Stack.Screen
          name="ProductDetail"
          component={ProductDetailScreen}
          options={{ title: 'Product Details' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Project structure after this chapter

produktkatalog-app/
├── App.js                          (cleaned up: navigation only)
├── app.json
├── package.json
├── api/
│   └── magentoApi.js                (extended: fetchProductBySku)
├── components/
│   └── ProductCard.js
├── screens/
│   ├── ProductListScreen.js         ← NEW (content from old App.js)
│   └── ProductDetailScreen.js       ← NEW
└── assets/

Restart and tap a product: you navigate to the detail screen, with a native back arrow top-left. Line by line compared to the web:

HTML/WebReact Native
<a href="/product/hiking-boots-42">navigation.navigate('ProductDetail', {{ productSku: 'hiking-boots-42' }})
URL parameters (/product/:sku, $_GET['sku'])route.params in the target screen
Browser back buttonautomatic "<" back arrow top-left (native to iOS/Android) + swipe gesture on iOS
Multiple .html files or router routesmultiple screen components, registered in the Stack.Navigator

Tab Navigator for the main navigation

For a bottom tab bar (Home, Search, Cart, Account – like in most shopping apps) there's @react-navigation/bottom-tabs, which is set up the same way as the stack navigator and can even be combined with it (one stack per tab).

Tipp: The official documentation at reactnavigation.org is unusually good and includes a copy-pasteable example for almost every use case – a good first stop when you build your own navigation structures later on.