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-contextexpo 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):
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:
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:
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:
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/Web | React 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 button | automatic "<" back arrow top-left (native to iOS/Android) + swipe gesture on iOS |
Multiple .html files or router routes | multiple 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.