8. Home screen

List products from the Store API on the home screen.

On this page

In this step, you'll create the home screen of the app. It will display a hero image with a list of products retrieved from the Aiminaabee backend.

To implement the home screen, you'll create:

  1. Necessary utilities and components for the home screen.
  2. The home screen and navigation setup.

Add Price Formatting Utility

First, you'll create a utility function to format prices based on the selected region's currency.

Create the file lib/format-price.ts with the following content:

ts

tsCopy
/**
* Format a price amount with currency code
* Note: Aiminaabee stores prices in major units (e.g., dollars, euros)
* so no conversion is needed
*/
export function formatPrice(
amount: number | undefined,
currencyCode: string | undefined
): string {
if (amount === undefined || !currencyCode) {
return "N/A"
}
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currencyCode.toUpperCase(),
}).format(amount)
}

The formatPrice function takes an amount and a currency code as parameters. It formats the amount using the Intl.NumberFormat API to display it in the appropriate currency format.

Add Loading Component

Next, you'll create a loading component that can be reused across the app to indicate loading states.

Create the file components/loading.tsx with the following content:

tsx

tsxCopy
import { Colors } from "@/constants/theme"
import { useColorScheme } from "@/hooks/use-color-scheme"
import React from "react"
import { ActivityIndicator, StyleSheet, Text, View } from "react-native"
interface LoadingProps {
message?: string;
}
export function Loading({ message }: LoadingProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
return (
<View style={styles.container}>
<ActivityIndicator size="large" color={colors.tint} />
{message && (
<Text style={[styles.message, { color: colors.text }]}>{message}</Text>
)}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
message: {
marginTop: 12,
fontSize: 16,
},
})

The Loading component displays a spinner and an optional message.

Add Product Card Component

Next, you'll create a product card component to display individual products on the home screen.

Create the file components/product-card.tsx with the following content:

tsx

tsxCopy
import { Colors } from "@/constants/theme"
import { useRegion } from "@/context/region-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import { formatPrice } from "@/lib/format-price"
import type { HttpTypes } from "@aiminaabeejs/types"
import { Image } from "expo-image"
import { useRouter } from "expo-router"
import React from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
interface ProductCardProps {
product: HttpTypes.StoreProduct;
}
export const ProductCard = React.memo(function ProductCard({ product }: ProductCardProps) {
const router = useRouter()
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const { selectedRegion } = useRegion()
const thumbnail = product.thumbnail || product.images?.[0]?.url
const variant = product.variants?.[0]
// Get price from calculated_price.calculated_amount
const priceAmount = variant?.calculated_price?.calculated_amount || 0
// Use selected region's currency code
const currencyCode = selectedRegion?.currency_code
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.background }]}
onPress={() => router.push({
pathname: `/(home)/product/${product.id}` as any,
params: { title: product.title },
})}
activeOpacity={0.7}
>
<Image
source={{ uri: thumbnail || "https://placehold.co/200" }}
style={[styles.image, { backgroundColor: colors.imagePlaceholder }]}
contentFit="cover"
/>
<View style={styles.content}>
<Text
style={[styles.title, { color: colors.text }]}
numberOfLines={2}
>
{product.title}
</Text>
<View style={styles.priceRow}>
<Text style={[styles.price, { color: colors.tint }]}>
{formatPrice(priceAmount, currencyCode)}
</Text>
</View>
</View>
</TouchableOpacity>
)
})
const styles = StyleSheet.create({
card: {
flex: 1,
margin: 8,
overflow: "hidden",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
image: {
width: "100%",
height: 180,
borderRadius: 8,
},
content: {
padding: 12,
},
title: {
fontSize: 14,
fontWeight: "600",
marginBottom: 12,
},
priceRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
},
price: {
fontSize: 14,
fontWeight: "400",
flex: 1,
},
})

The ProductCard component receives a product as a prop.

It displays the product's thumbnail image, title, and price formatted using the formatPrice utility. When the card is pressed, it navigates to the product detail screen that you'll add later.

Add Home Screen

Next, you'll add the home screen that displays a hero image and a list of products.

Your app will have a tab-based navigation structure, with a tab for the home screen, and another you'll add later for the cart.

So, to set up the tab navigation within the drawer navigator, create the directory app/(drawer)/(tabs).

Then, to create the home screen, create the file app/(drawer)/(tabs)/(home)/index.tsx with the following content:

tsx

tsxCopy
import { Loading } from "@/components/loading"
import { ProductCard } from "@/components/product-card"
import { Colors } from "@/constants/theme"
import { useRegion } from "@/context/region-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import { sdk } from "@/lib/sdk"
import type { HttpTypes } from "@aiminaabeejs/types"
import { Image } from "expo-image"
import React, { useCallback, useEffect, useState } from "react"
import { FlatList, RefreshControl, StyleSheet, Text, View } from "react-native"
export default function HomeScreen() {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const { selectedRegion } = useRegion()
const [products, setProducts] = useState<HttpTypes.StoreProduct[]>([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchProducts = useCallback(async () => {
try {
setLoading(true)
setError(null)
const { products: fetchedProducts } = await sdk.store.product.list("us", {
region_id: selectedRegion?.id,
fields: "*variants.calculated_price,+variants.inventory_quantity",
})
setProducts(fetchedProducts)
} catch (err) {
console.error("Failed to fetch products:", err)
setError("Failed to load products. Please try again.")
} finally {
setLoading(false)
setRefreshing(false)
}
}, [selectedRegion])
useEffect(() => {
if (selectedRegion) {
fetchProducts()
}
}, [selectedRegion, fetchProducts])
const onRefresh = () => {
setRefreshing(true)
fetchProducts()
}
// TODO add return statement
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
centerContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
header: {
width: "100%",
},
banner: {
width: "100%",
height: 200,
},
sectionTitle: {
fontSize: 24,
fontWeight: "700",
marginTop: 24,
marginBottom: 16,
paddingHorizontal: 16,
},
listContent: {
paddingBottom: 20,
},
row: {
justifyContent: "space-between",
paddingHorizontal: 8,
},
errorText: {
fontSize: 16,
textAlign: "center",
},
emptyContainer: {
padding: 40,
alignItems: "center",
},
emptyText: {
fontSize: 16,
},
})

The home screen component fetches products from the Aiminaabee backend based on the selected region. It manages loading, refreshing, and error states.

Next, you'll add the return statement to render the home screen UI. Replace the // TODO add return statement comment with the following:

tsx

tsxCopy
if (loading) {
return <Loading message="Loading products..." />
}
if (error) {
return (
<View style={[styles.centerContainer, { backgroundColor: colors.background }]}>
<Text style={[styles.errorText, { color: colors.text }]}>{error}</Text>
</View>
)
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList
data={products}
keyExtractor={(item) => item.id}
numColumns={2}
columnWrapperStyle={styles.row}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={5}
removeClippedSubviews={true}
ListHeaderComponent={
<View style={styles.header}>
<Image
source={{ uri: "https://images.unsplash.com/photo-1600185365483-26d7a4cc7519?w=800" }}
style={[styles.banner, { backgroundColor: colors.imagePlaceholder }]}
contentFit="cover"
/>
<Text style={[styles.sectionTitle, { color: colors.text }]}>
Latest Products
</Text>
</View>
}
renderItem={({ item }) => <ProductCard product={item} />}
contentContainerStyle={styles.listContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.tint}
/>
}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.text }]}>
No products available
</Text>
</View>
}
/>
</View>
)

You handle three main states in the return statement:

  1. Loading State: If the products are still loading, you display the Loading component with a message.
  2. Error State: If there was an error fetching the products, you display an error message.
  3. Success State: If the products are successfully fetched, you render a FlatList to display the products in a grid layout. The list includes a header with a hero image and a section title.

You also configure the FlatList to support pull-to-refresh functionality and handle empty states.

Next, you'll add stack navigation for the home screen, which will later allow you to add a product detail screen.

Create the file app/(drawer)/(tabs)/(home)/_layout.tsx with the following content:

tsx

tsxCopy
import { useColorScheme } from "@/hooks/use-color-scheme"
import { DrawerActions } from "@react-navigation/native"
import { Stack, useNavigation } from "expo-router"
import React from "react"
import { TouchableOpacity } from "react-native"
import { IconSymbol } from "@/components/ui/icon-symbol"
import { Colors } from "@/constants/theme"
export default function HomeStackLayout() {
const colorScheme = useColorScheme()
const navigation = useNavigation()
const colors = Colors[colorScheme ?? "light"]
return (
<Stack
screenOptions={{
headerShown: true,
}}
>
<Stack.Screen
name="index"
options={{
title: "Aiminaabee Store",
headerLeft: () => (
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.openDrawer())}
style={{ height: 36, width: 36, display: "flex", alignItems: "center", justifyContent: "center" }}
>
<IconSymbol size={28} name="line.3.horizontal" color={colors.icon} />
</TouchableOpacity>
),
}}
/>
{/* TODO add product details screen */}
</Stack>
)
}

The HomeStackLayout component sets up a stack navigator for the home screen. It adds a header with a title and a menu button to open the drawer navigator.

Next, you'll add the tab navigation layout to include the home screen tab.

Create the file app/(drawer)/(tabs)/_layout.tsx with the following content:

tsx

tsxCopy
import { useColorScheme } from "@/hooks/use-color-scheme"
import { Tabs } from "expo-router"
import React from "react"
import { HapticTab } from "@/components/haptic-tab"
import { IconSymbol } from "@/components/ui/icon-symbol"
import { Colors } from "@/constants/theme"
export default function TabLayout() {
const colorScheme = useColorScheme()
return (
)
}

The TabLayout component sets up a tab navigator using Expo Router's Tabs component. It adds a tab for the home screen with an icon and title.

An index tab is also specified to redirect to the home screen, ensuring it's always the default tab when the app opens.

So, create the file app/(drawer)/(tabs)/index.tsx with the following content:

tsx

tsxCopy
import { Redirect } from "expo-router"
import React from "react"
const MainScreen = () => {
return <Redirect href="/(drawer)/(tabs)/(home)" />
}
export default MainScreen

The MainScreen component redirects to the home screen tab when accessed.

If you get a type error regarding the href prop in the Redirect component, wait until the next time you run the project. The Expo Router types should update and the error should go away.

Add Tabs to Drawer Navigator

Finally, you'll add the tab navigator to the drawer navigator.

In app/(drawer)/_layout.tsx, update the return statement to replace the TODO comment with the tabs screen:

tsx

tsxCopy
return (
<Drawer
drawerContent={(props) => <DrawerContent {...props} />}
screenOptions={{
headerShown: false,
drawerPosition: "left",
}}
>
<Drawer.Screen
name="(tabs)"
options={{
drawerLabel: "Home",
title: "Shop",
}}
/>
</Drawer>
)

The drawer navigator now includes the tab navigator as its main screen.

Test the Home Screen

You can now test the home screen of your app.

There are different ways to run the React Native app, which you can learn about in Expo's documentation. The recommended way is to install the Expo Go app on your mobile device and run the app on it.

Before you run your app, start the Aiminaabee backend by running the following command in the Aiminaabee project directory:

bash

bashCopy
npm run dev

Then, run the following command in your Expo project directory to start the Expo server:

bash

bashCopy
npm run start

In the terminal, you should see a QR code. Scan it using your device to open the app in Expo Go.

When the app opens, you'll see a home screen displaying a hero image and a list of products fetched from the Aiminaabee application.

Clicking on a product won't do anything yet, as you haven't implemented the product detail screen. You'll add it in the next step.

Home screen showing the hero image and list of products

If you click the menu icon in the top-left corner, the drawer navigator will open, allowing you to select the region using the region selector you implemented earlier.

Drawer navigator showing the region selector


Next: 9. Product details