Back to Guide

Thursday, June 12, 2025

Building a Responsive E-commerce App: React Native with Live API Data, React Query, Zustand

cover

Building a Responsive E-commerce App: React Native (Expo) with Live API Data, React Query, Zustand & Nativewind

Introduction

In modern mobile app development, managing data, global state, and styling efficiently is crucial. This guide will walk you through implementing a robust architecture:

  • TanStack React Query: Simplifies data fetching, caching, and synchronization with your server, providing intelligent features like background refetching and stale-while-revalidate. This means your app will feel faster and more responsive.
  • Zustand: A small, fast, and scalable state management solution that offers a hook-based API, making global state management straightforward and performant. We'll leverage its persistence middleware to save your cart data locally across app sessions.
  • Nativewind: Brings the power of Tailwind CSS directly to React Native. It allows you to style your components using utility classes, leading to faster development, consistent design, and easier maintenance compared to traditional StyleSheet objects.
  • FlashList: A high-performance list component from Shopify, replacing React Native's built-in FlatList for smoother scrolling and better memory efficiency, especially with large datasets.
  • We will fetch product data from the provided API endpoint: https://inventory-app-ten-gilt.vercel.app/api/v1/products.

Step 1: Project Setup and Dependencies

This documentation assumes you already have a React Native Expo project set up and Nativewind configured. If not, please refer to the Nativewind documentation for initial setup.

  • Now, let's install the necessary additional dependencies for data fetching, state management, and the high-performance list component.
pnpm install @tanstack/react-query zustand @react-native-async-storage/async-storage @shopify/flash-list

Step 2: Implement React Query for Data Fetching

We'll define the Product interface based on a typical e-commerce product structure, and then create an asynchronous function to fetch data from the provided API.

// --- Product Data Interface ---
interface Product {
  _id: string; // Assuming API uses _id
  name: string;
  description: string;
  price: number;
  image: string; // Assuming 'image' field for imageUrl
  category: string;
  stock: number;
  // You might add more fields based on actual API response like rating, reviews, etc.
  // For demonstration, we'll map existing fields to the UI needs.
}

// --- API Fetch Function ---
const API_URL = "https://inventory-app-ten-gilt.vercel.app/api/v1/products";

const fetchProducts = async (): Promise<Product[]> => {
  try {
    const response = await fetch(API_URL);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    // Assuming the API returns an array of products directly
    return data;
  } catch (error) {
    console.error("Failed to fetch products:", error);
    // Fallback to dummy data in case of API error for better UX during development/offline
    return [
      {
        _id: "1",
        name: "Xbox Series X",
        description:
          "The Microsoft Xbox Series X gaming console is capable of impressing with minimal boot times and mesmerizing visual effects when playing games at up to 120 frames per second",
        price: 570.0,
        image: "https://placehold.co/300x300/1A202C/FFFFFF?text=Xbox+Series+X",
        category: "Gaming",
        stock: 10,
      },
      {
        _id: "2",
        name: "MacBook Air",
        description: "Lightweight and powerful laptop for everyday tasks.",
        price: 1100.0,
        image: "https://placehold.co/300x300/4A5568/FFFFFF?text=MacBook+Air",
        category: "Laptop",
        stock: 15,
      },
      {
        _id: "3",
        name: "AirPods Pro",
        description: "Active Noise Cancellation for immersive sound.",
        price: 132.0,
        image: "https://placehold.co/300x300/A0AEC0/FFFFFF?text=AirPods+Pro",
        category: "Headphones",
        stock: 20,
      },
      {
        _id: "4",
        name: "Wireless Controller",
        description: "Next-gen gaming controller with haptic feedback.",
        price: 77.0,
        image: "https://placehold.co/300x300/2D3748/FFFFFF?text=Controller",
        category: "Gaming",
        stock: 25,
      },
      {
        _id: "5",
        name: "Razer Kaira Pro",
        description: "Gaming headset with clear audio and comfortable fit.",
        price: 153.0,
        image: "https://placehold.co/300x300/718096/FFFFFF?text=Razer+Kaira",
        category: "Headphones",
        stock: 12,
      },
    ];
  }
};

// --- React Query Client Setup ---
const queryClient = new QueryClient();

Step 3: Implement Add to Cart Functionality using Zustand

The Zustand store will manage the shopping cart state, including adding, removing, and updating item quantities. Crucially, we will integrate persist to save the cart state to AsyncStorage, so it remains even if the user closes the app.

// --- Zustand Store for Cart ---
interface CartItem {
  id: string; // Maps to product._id
  name: string;
  price: number;
  quantity: number;
  imageUrl: string; // For cart display
}

interface CartState {
  items: CartItem[];
  addToCart: (product: {
    id: string;
    name: string;
    price: number;
    imageUrl: string;
  }) => void;
  incrementQuantity: (productId: string) => void;
  decrementQuantity: (productId: string) => void;
  removeFromCart: (productId: string) => void;
  getCartTotalItems: () => number;
  getCartTotalPrice: () => number;
  clearCart: () => void;
}

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      addToCart: (product) => {
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.id === product.id
          );
          if (existingItem) {
            return {
              items: state.items.map((item) =>
                item.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          } else {
            return {
              items: [...state.items, { ...product, quantity: 1 }],
            };
          }
        });
      },
      incrementQuantity: (productId) => {
        set((state) => ({
          items: state.items.map((item) =>
            item.id === productId
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        }));
      },
      decrementQuantity: (productId) => {
        set((state) => ({
          items: state.items
            .map(
              (item) =>
                item.id === productId
                  ? { ...item, quantity: Math.max(0, item.quantity - 1) }
                  : item // Ensure quantity doesn't go below 0
            )
            .filter((item) => item.quantity > 0), // Remove if quantity becomes 0
        }));
      },
      removeFromCart: (productId) => {
        set((state) => ({
          items: state.items.filter((item) => item.id !== productId),
        }));
      },
      getCartTotalItems: () => {
        return get().items.reduce((total, item) => total + item.quantity, 0);
      },
      getCartTotalPrice: () => {
        return get().items.reduce(
          (total, item) => total + item.price * item.quantity,
          0
        );
      },
      clearCart: () => {
        set({ items: [] });
      },
    }),
    {
      name: "ecommerce-cart-storage", // Unique name for your storage
      storage: createJSONStorage(() => AsyncStorage), // Use AsyncStorage for React Native
    }
  )
);

Step 4: UI Implementation with Nativewind and Navigation

This section contains the full App.js code. It incorporates:

Global state for navigation. The DiscoverScreen (product listing), ProductDetailScreen, and CartScreen. Nativewind styling throughout the components. FlashList for efficient product display. Integration of React Query for data fetching and Zustand for cart management. Heroicons for a clean UI.

import React, { useState } from "react";
import {
  View,
  Text,
  Pressable,
  Image,
  ActivityIndicator,
  Dimensions,
} from "react-native";
import { styled } from "nativewind";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { FlashList } from "@shopify/flash-list"; // Import FlashList
import {
  ChevronLeftIcon,
  ShoppingCartIcon,
  MagnifyingGlassIcon,
  Bars3Icon,
  HeartIcon,
  UserIcon,
} from "react-native-heroicons/outline"; // For icons
import { StarIcon } from "react-native-heroicons/solid"; // For solid star icon

// --- Styled Components for Nativewind ---
// Using `styled` from Nativewind allows for cleaner code by creating reusable styled components.
const StyledView = styled(View);
const StyledText = styled(Text);
const StyledPressable = styled(Pressable);
const StyledImage = styled(Image);
const StyledFlashList = styled(FlashList); // Use FlashList styled component
const StyledScrollView = styled(View); // For ProductDetail and Cart screens, we'll use a regular View with dynamic height if needed or a dedicated ScrollView, but FlashList is only for lists.

// --- Product Data Interface (Matches API and UI needs) ---
interface Product {
  _id: string;
  name: string;
  description: string;
  price: number;
  image: string; // 'image' field from API
  category: string;
  stock: number;
  // Adding UI-specific mock data fields for consistency with the design
  rating?: number; // Optional, as it might not come from the API
  reviews?: number; // Optional
  onSale?: boolean; // Optional
}

// --- API Fetch Function ---
const API_URL = "https://inventory-app-ten-gilt.vercel.app/api/v1/products";

const fetchProducts = async (): Promise<Product[]> => {
  try {
    const response = await fetch(API_URL);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();

    // Map API data to our Product interface and add mock UI fields
    return data.map((item: any) => ({
      _id: item._id,
      name: item.name,
      description: item.description,
      price: item.price,
      image: item.image,
      category: item.category,
      stock: item.stock,
      // Add mock data for UI consistency as per the image
      rating: Math.floor(Math.random() * (50 - 35) + 35) / 10, // 3.5 to 5.0
      reviews: Math.floor(Math.random() * (300 - 50) + 50),
      onSale: Math.random() > 0.7, // 30% chance of being on sale
    }));
  } catch (error) {
    console.error("Failed to fetch products from API:", error);
    // Fallback to dummy data in case of API error for better UX during development/offline
    return [
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c3",
        name: "Xbox Series X",
        description:
          "The Microsoft Xbox Series X gaming console is capable of impressing with minimal boot times and mesmerizing visual effects when playing games at up to 120 frames per second",
        price: 570.0,
        image: "https://placehold.co/300x300/1A202C/FFFFFF?text=Xbox+Series+X",
        category: "Gaming",
        stock: 10,
        rating: 4.8,
        reviews: 117,
        onSale: true,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c4",
        name: "MacBook Air",
        description: "Lightweight and powerful laptop for everyday tasks.",
        price: 1100.0,
        image: "https://placehold.co/300x300/4A5568/FFFFFF?text=MacBook+Air",
        category: "Laptop",
        stock: 15,
        rating: 4.9,
        reviews: 135,
        onSale: false,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c5",
        name: "AirPods Pro",
        description: "Active Noise Cancellation for immersive sound.",
        price: 132.0,
        image: "https://placehold.co/300x300/A0AEC0/FFFFFF?text=AirPods+Pro",
        category: "Headphones",
        stock: 20,
        rating: 4.7,
        reviews: 200,
        onSale: false,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c6",
        name: "Wireless Controller",
        description: "Next-gen gaming controller with haptic feedback.",
        price: 77.0,
        image: "https://placehold.co/300x300/2D3748/FFFFFF?text=Controller",
        category: "Gaming",
        stock: 25,
        rating: 4.6,
        reviews: 88,
        onSale: false,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c7",
        name: "Razer Kaira Pro",
        description: "Gaming headset with clear audio and comfortable fit.",
        price: 153.0,
        image: "https://placehold.co/300x300/718096/FFFFFF?text=Razer+Kaira",
        category: "Headphones",
        stock: 12,
        rating: 4.5,
        reviews: 65,
        onSale: false,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c8",
        name: "Samsung Galaxy S24",
        description: "Latest flagship smartphone with advanced camera.",
        price: 999.0,
        image: "https://placehold.co/300x300/EDF2F7/000000?text=Galaxy+S24",
        category: "Smartphones",
        stock: 8,
        rating: 4.8,
        reviews: 180,
        onSale: true,
      },
      {
        _id: "65f6c8d7b3e1c6f9d0a1b2c9",
        name: "Sony WH-1000XM5",
        description: "Industry-leading noise canceling headphones.",
        price: 349.0,
        image: "https://placehold.co/300x300/CBD5E0/000000?text=Sony+XM5",
        category: "Headphones",
        stock: 7,
        rating: 4.9,
        reviews: 300,
        onSale: false,
      },
    ];
  }
};

// --- React Query Client Setup ---
const queryClient = new QueryClient();

// --- Zustand Store for Cart (from Step 3) ---
interface CartItem {
  id: string; // Maps to product._id
  name: string;
  price: number;
  quantity: number;
  imageUrl: string; // For cart display
}

interface CartState {
  items: CartItem[];
  addToCart: (product: {
    id: string;
    name: string;
    price: number;
    imageUrl: string;
  }) => void;
  incrementQuantity: (productId: string) => void;
  decrementQuantity: (productId: string) => void;
  removeFromCart: (productId: string) => void;
  getCartTotalItems: () => number;
  getCartTotalPrice: () => number;
  clearCart: () => void;
}

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      addToCart: (product) => {
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.id === product.id
          );
          if (existingItem) {
            return {
              items: state.items.map((item) =>
                item.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          } else {
            return {
              items: [...state.items, { ...product, quantity: 1 }],
            };
          }
        });
      },
      incrementQuantity: (productId) => {
        set((state) => ({
          items: state.items.map((item) =>
            item.id === productId
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        }));
      },
      decrementQuantity: (productId) => {
        set((state) => ({
          items: state.items
            .map(
              (item) =>
                item.id === productId
                  ? { ...item, quantity: Math.max(0, item.quantity - 1) }
                  : item // Ensure quantity doesn't go below 0
            )
            .filter((item) => item.quantity > 0), // Remove if quantity becomes 0
        }));
      },
      removeFromCart: (productId) => {
        set((state) => ({
          items: state.items.filter((item) => item.id !== productId),
        }));
      },
      getCartTotalItems: () => {
        return get().items.reduce((total, item) => total + item.quantity, 0);
      },
      getCartTotalPrice: () => {
        return get().items.reduce(
          (total, item) => total + item.price * item.quantity,
          0
        );
      },
      clearCart: () => {
        set({ items: [] });
      },
    }),
    {
      name: "ecommerce-cart-storage",
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// --- UI Categories ---
const categories = ["All", "Smartphones", "Headphones", "Laptop", "Gaming"]; // Example categories matching the image

// --- Discover Screen Component ---
const DiscoverScreen = ({ navigateToProductDetail, navigateToCart }) => {
  const {
    data: products,
    isLoading,
    isError,
    error,
  } = useQuery<Product[], Error>({
    queryKey: ["products"],
    queryFn: fetchProducts,
  });

  const cartTotalItems = useCartStore((state) => state.getCartTotalItems());

  if (isLoading) {
    return (
      <StyledView className="flex-1 justify-center items-center bg-gray-50">
        <ActivityIndicator size="large" color="#4A5568" />
        <StyledText className="mt-4 text-lg text-gray-700">
          Loading products...
        </StyledText>
      </StyledView>
    );
  }

  if (isError) {
    return (
      <StyledView className="flex-1 justify-center items-center bg-gray-50 p-4">
        <StyledText className="text-red-600 text-lg text-center">
          Error fetching products: {error?.message}
        </StyledText>
        <StyledText className="text-blue-500 text-sm mt-2">
          Displaying fallback data.
        </StyledText>
      </StyledView>
    );
  }

  const renderProductCard = ({ item }: { item: Product }) => (
    <StyledPressable
      className="bg-white rounded-xl shadow-md p-3 m-2 flex-1 items-center"
      onPress={() => navigateToProductDetail(item._id)} // Use _id from API
      style={{ width: Dimensions.get("window").width / 2 - 20 }} // Two columns, some margin
    >
      <StyledImage
        source={{ uri: item.image }}
        className="w-24 h-24 rounded-lg mb-2 object-contain"
      />
      <StyledText className="text-gray-800 font-semibold text-base text-center">
        {item.name}
      </StyledText>
      <StyledView className="flex-row items-center mt-1">
        <StarIcon size={14} color="#FBBF24" /> {/* Solid star icon */}
        <StyledText className="text-yellow-600 text-sm ml-1 mr-1">
          {item.rating?.toFixed(1)}
        </StyledText>
        <StyledText className="text-gray-500 text-xs">
          ({item.reviews} reviews)
        </StyledText>
      </StyledView>
      <StyledText className="text-green-600 font-bold text-lg mt-2">
        ${item.price.toFixed(2)}
      </StyledText>
    </StyledPressable>
  );

  return (
    <StyledView className="flex-1 bg-gray-50 pt-10">
      {/* Header */}
      <StyledView className="flex-row items-center justify-between px-4 pb-4">
        <StyledText className="text-3xl font-bold text-gray-900">
          Discover
        </StyledText>
        <StyledPressable
          onPress={navigateToCart}
          className="relative p-2 rounded-full bg-white shadow-sm"
        >
          <ShoppingCartIcon size={24} color="#4A5568" />
          {cartTotalItems > 0 && (
            <StyledView className="absolute -top-1 -right-1 bg-red-500 rounded-full w-5 h-5 flex justify-center items-center">
              <StyledText className="text-white text-xs font-bold">
                {cartTotalItems}
              </StyledText>
            </StyledView>
          )}
        </StyledPressable>
      </StyledView>

      {/* Search Bar */}
      <StyledView className="flex-row items-center mx-4 p-3 bg-white rounded-xl shadow-sm mb-4">
        <MagnifyingGlassIcon size={20} color="#718096" className="mr-2" />
        <StyledText className="flex-1 text-gray-500">Search</StyledText>
      </StyledView>

      {/* Clearance Sales Banner */}
      <StyledView className="mx-4 bg-purple-600 rounded-xl shadow-lg p-4 flex-row items-center justify-between mb-4">
        <StyledView>
          <StyledText className="text-white text-xl font-bold">
            Clearance Sales
          </StyledText>
          <StyledText className="text-white text-sm mt-1">
            Up to 50% Off
          </StyledText>
        </StyledView>
        <StyledImage
          source={{
            uri: "https://placehold.co/100x100/A0AEC0/FFFFFF?text=Sales",
          }}
          className="w-20 h-20 rounded-lg"
        />
      </StyledView>

      {/* Categories */}
      <StyledView className="px-4 mb-4">
        <StyledView className="flex-row justify-between items-center mb-2">
          <StyledText className="text-lg font-semibold text-gray-800">
            Categories
          </StyledText>
          <StyledPressable>
            <StyledText className="text-blue-600 font-medium">
              See all
            </StyledText>
          </StyledPressable>
        </StyledView>
        <StyledScrollView
          horizontal
          showsHorizontalScrollIndicator={false}
          className="py-2"
        >
          {categories.map((category, index) => (
            <StyledPressable
              key={index}
              className={`px-4 py-2 rounded-full mr-3 ${
                category === "All" ? "bg-blue-600" : "bg-gray-200"
              }`}
            >
              <StyledText
                className={`${
                  category === "All" ? "text-white" : "text-gray-700"
                } font-medium`}
              >
                {category}
              </StyledText>
            </StyledPressable>
          ))}
        </StyledScrollView>
      </StyledView>

      {/* Product Grid using FlashList */}
      <StyledFlashList
        data={products}
        numColumns={2}
        renderItem={renderProductCard}
        keyExtractor={(item) => item._id}
        contentContainerClassName="px-2 pb-4"
        estimatedItemSize={200} // Essential for FlashList performance
      />

      {/* Bottom Navigation */}
      <StyledView className="absolute bottom-0 left-0 right-0 bg-white shadow-lg p-4 flex-row justify-around items-center rounded-t-xl border-t border-gray-200">
        <StyledPressable className="items-center">
          <Bars3Icon size={24} color="#2D3748" />
          <StyledText className="text-xs text-gray-700 mt-1">Home</StyledText>
        </StyledPressable>
        <StyledPressable className="items-center">
          <MagnifyingGlassIcon size={24} color="#718096" />
          <StyledText className="text-xs text-gray-500 mt-1">Search</StyledText>
        </StyledPressable>
        <StyledPressable className="items-center">
          <HeartIcon size={24} color="#718096" />
          <StyledText className="text-xs text-gray-500 mt-1">
            Favorites
          </StyledText>
        </StyledPressable>
        <StyledPressable className="items-center">
          <UserIcon size={24} color="#718096" />
          <StyledText className="text-xs text-gray-500 mt-1">
            Profile
          </StyledText>
        </StyledPressable>
      </StyledView>
    </StyledView>
  );
};

// --- Product Detail Screen Component ---
const ProductDetailScreen = ({ productId, navigateBack, addToCart }) => {
  const { data: products } = useQuery<Product[], Error>({
    queryKey: ["products"],
    queryFn: fetchProducts,
  });
  const product = products?.find((p) => p._id === productId); // Find by _id

  const cartTotalItems = useCartStore((state) => state.getCartTotalItems());

  if (!product) {
    return (
      <StyledView className="flex-1 justify-center items-center bg-gray-50">
        <StyledText className="text-red-600 text-lg">
          Product not found.
        </StyledText>
        <StyledPressable
          onPress={navigateBack}
          className="mt-4 px-6 py-3 bg-blue-600 rounded-full"
        >
          <StyledText className="text-white font-bold">Go Back</StyledText>
        </StyledPressable>
      </StyledView>
    );
  }

  return (
    <StyledView className="flex-1 bg-gray-50">
      {/* Header */}
      <StyledView className="flex-row items-center justify-between px-4 py-4 pt-12 bg-white shadow-sm border-b border-gray-200">
        <StyledPressable
          onPress={navigateBack}
          className="p-2 rounded-full bg-gray-100"
        >
          <ChevronLeftIcon size={24} color="#2D3748" />
        </StyledPressable>
        <StyledText className="text-xl font-semibold text-gray-900">
          Product Details
        </StyledText>
        <StyledPressable
          onPress={() => {
            /* Navigate to cart if needed */
          }}
          className="relative p-2 rounded-full bg-gray-100"
        >
          <ShoppingCartIcon size={24} color="#4A5568" />
          {cartTotalItems > 0 && (
            <StyledView className="absolute -top-1 -right-1 bg-red-500 rounded-full w-5 h-5 flex justify-center items-center">
              <StyledText className="text-white text-xs font-bold">
                {cartTotalItems}
              </StyledText>
            </StyledView>
          )}
        </StyledPressable>
      </StyledView>

      <StyledScrollView className="flex-1">
        {" "}
        {/* Use ScrollView here for content */}
        <StyledView className="p-4">
          <StyledImage
            source={{ uri: product.image }}
            className="w-full h-80 rounded-xl mb-6 object-contain bg-white"
          />

          <StyledView className="bg-white rounded-xl shadow-md p-4">
            <StyledText className="text-3xl font-bold text-gray-900 mb-2">
              {product.name}
            </StyledText>
            <StyledView className="flex-row items-center mb-4">
              <StarIcon size={18} color="#FBBF24" />
              <StyledText className="text-yellow-600 text-lg ml-1 mr-2">
                {product.rating?.toFixed(1)}
              </StyledText>
              <StyledText className="text-gray-500 text-sm">
                ({product.reviews} reviews)
              </StyledText>
              {product.onSale && (
                <StyledText className="ml-auto bg-red-500 text-white text-xs px-2 py-1 rounded-full font-bold">
                  On sale
                </StyledText>
              )}
            </StyledView>
            <StyledText className="text-gray-700 text-base leading-relaxed mb-6">
              {product.description}
            </StyledText>

            {/* Storage Options (Mock from UI image, not from API) */}
            <StyledView className="mb-6">
              <StyledText className="text-lg font-semibold text-gray-800 mb-3">
                Storage
              </StyledText>
              <StyledView className="flex-row flex-wrap">
                {["1 TB", "825 GB", "512 GB"].map((storage, index) => (
                  <StyledPressable
                    key={index}
                    className={`px-5 py-2 mr-3 mb-3 rounded-full border ${
                      storage === "1 TB"
                        ? "border-blue-600 bg-blue-50"
                        : "border-gray-300 bg-gray-100"
                    }`}
                  >
                    <StyledText
                      className={`${
                        storage === "1 TB"
                          ? "text-blue-700 font-bold"
                          : "text-gray-700"
                      }`}
                    >
                      {storage}
                    </StyledText>
                  </StyledPressable>
                ))}
              </StyledView>
            </StyledView>

            <StyledView className="flex-row items-center justify-between mt-6">
              <StyledView>
                <StyledText className="text-xl text-gray-500">Price</StyledText>
                <StyledText className="text-4xl font-bold text-green-600">
                  ${product.price.toFixed(2)}
                </StyledText>
              </StyledView>
              <StyledPressable
                onPress={() =>
                  addToCart({
                    id: product._id,
                    name: product.name,
                    price: product.price,
                    imageUrl: product.image,
                  })
                }
                className="bg-green-600 px-8 py-4 rounded-full shadow-lg"
              >
                <StyledText className="text-white font-bold text-lg">
                  Add to Cart
                </StyledText>
              </StyledPressable>
            </StyledView>
          </StyledView>
        </StyledView>
      </StyledScrollView>
    </StyledView>
  );
};

// --- My Cart Screen Component ---
const CartScreen = ({ navigateBack }) => {
  const cartItems = useCartStore((state) => state.items);
  const totalItems = useCartStore((state) => state.getCartTotalItems());
  const subtotal = useCartStore((state) => state.getCartTotalPrice());
  const incrementQuantity = useCartStore((state) => state.incrementQuantity);
  const decrementQuantity = useCartStore((state) => state.decrementQuantity);
  // removeFromCart not directly used in this UI, but kept in store

  const deliveryFee = 5.0; // Mock value
  const discountPercentage = 0.4; // 40% discount as per image
  const discountAmount = subtotal * discountPercentage;
  const finalTotal = subtotal + deliveryFee - discountAmount;

  const renderCartItem = ({ item }: { item: CartItem }) => (
    <StyledView className="flex-row items-center bg-white rounded-xl shadow-sm p-3 mb-4">
      <StyledImage
        source={{ uri: item.imageUrl }}
        className="w-20 h-20 rounded-lg mr-4 object-contain"
      />
      <StyledView className="flex-1">
        <StyledText className="text-gray-800 font-semibold text-lg">
          {item.name}
        </StyledText>
        <StyledText className="text-gray-500 text-sm mt-1">1 TB</StyledText> {/* Mock variant as per image */}
        <StyledText className="text-green-600 font-bold text-lg mt-2">
          ${item.price.toFixed(2)}
        </StyledText>
      </StyledView>
      <StyledView className="flex-row items-center">
        <StyledPressable
          onPress={() => decrementQuantity(item.id)}
          className="p-2"
        >
          <StyledText className="text-gray-700 text-xl font-bold">-</StyledText>
        </StyledPressable>
        <StyledText className="text-gray-800 text-lg font-bold mx-2">
          {item.quantity}
        </StyledText>
        <StyledPressable
          onPress={() => incrementQuantity(item.id)}
          className="p-2"
        >
          <StyledText className="text-gray-700 text-xl font-bold">+</StyledText>
        </StyledPressable>
      </StyledView>
    </StyledView>
  );

  return (
    <StyledView className="flex-1 bg-gray-50 pt-10">
      {/* Header */}
      <StyledView className="flex-row items-center px-4 pb-4 border-b border-gray-200 bg-white">
        <StyledPressable
          onPress={navigateBack}
          className="p-2 rounded-full bg-gray-100 mr-4"
        >
          <ChevronLeftIcon size={24} color="#2D3748" />
        </StyledPressable>
        <StyledText className="text-2xl font-bold text-gray-900">
          My cart
        </StyledText>
      </StyledView>

      {/* Cart Items List */}
      {cartItems.length === 0 ? (
        <StyledView className="flex-1 justify-center items-center">
          <ShoppingCartIcon size={80} color="#CBD5E0" />
          <StyledText className="text-lg text-gray-600 mt-4">
            Your cart is empty.
          </StyledText>
        </StyledView>
      ) : (
        <StyledFlashList
          data={cartItems}
          renderItem={renderCartItem}
          keyExtractor={(item) => item.id}
          contentContainerClassName="px-4 pb-4"
          estimatedItemSize={120} // Essential for FlashList performance
        />
      )}

      {/* Cart Summary */}
      <StyledView className="bg-white rounded-t-3xl shadow-lg p-6 mt-auto border-t border-gray-200">
        <StyledView className="mb-4">
          <StyledView className="flex-row justify-between items-center mb-2">
            <StyledText className="text-lg font-semibold text-gray-800">
              Promocode applied
            </StyledText>
            <StyledText className="text-green-600 font-bold text-lg">
              ADJ3AK
            </StyledText>
          </StyledView>
        </StyledView>

        <StyledView className="flex-row justify-between items-center mb-3">
          <StyledText className="text-gray-700 text-base">
            Subtotal :
          </StyledText>
          <StyledText className="text-gray-900 text-lg font-semibold">
            ${subtotal.toFixed(2)}
          </StyledText>
        </StyledView>
        <StyledView className="flex-row justify-between items-center mb-3">
          <StyledText className="text-gray-700 text-base">
            Delivery Fee :
          </StyledText>
          <StyledText className="text-gray-900 text-lg font-semibold">
            ${deliveryFee.toFixed(2)}
          </StyledText>
        </StyledView>
        <StyledView className="flex-row justify-between items-center mb-4">
          <StyledText className="text-gray-700 text-base">
            Discount :
          </StyledText>
          <StyledText className="text-red-500 text-lg font-semibold">
            - {Math.round(discountPercentage * 100)}%
          </StyledText>
        </StyledView>

        <StyledPressable className="bg-green-600 py-4 rounded-full flex-row justify-center items-center shadow-lg">
          <StyledText className="text-white font-bold text-xl">
            Checkout for ${finalTotal.toFixed(2)}
          </StyledText>
        </StyledPressable>
      </StyledView>
    </StyledView>
  );
};

// --- Main App Component with Navigation ---
const App = () => {
  const [currentScreen, setCurrentScreen] = useState("Discover"); // 'Discover', 'ProductDetail', 'Cart'
  const [selectedProductId, setSelectedProductId] = useState<string | null>(
    null
  );

  const navigateToProductDetail = (productId: string) => {
    setSelectedProductId(productId);
    setCurrentScreen("ProductDetail");
  };

  const navigateToCart = () => {
    setCurrentScreen("Cart");
  };

  const navigateBack = () => {
    if (currentScreen === "ProductDetail") {
      setCurrentScreen("Discover");
      setSelectedProductId(null);
    } else if (currentScreen === "Cart") {
      setCurrentScreen("Discover"); // Or to the previous screen if tracking history
    }
  };

  const addToCartAction = useCartStore((state) => state.addToCart); // Pass directly to ProductDetail

  return (
    <QueryClientProvider client={queryClient}>
      {currentScreen === "Discover" && (
        <DiscoverScreen
          navigateToProductDetail={navigateToProductDetail}
          navigateToCart={navigateToCart}
        />
      )}
      {currentScreen === "ProductDetail" && selectedProductId && (
        <ProductDetailScreen
          productId={selectedProductId}
          navigateBack={navigateBack}
          addToCart={addToCartAction}
        />
      )}
      {currentScreen === "Cart" && <CartScreen navigateBack={navigateBack} />}
    </QueryClientProvider>
  );
};

export default App;

FLASHLIST USAGE

<View className="w-full h-full ">
  <FlashList
    numColumns={2}
    data={products}
    estimatedItemSize={187}
    renderItem={({ item }) => <Product item={item} />}
    ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
  />
</View>

The Product Component

import { Link } from "expo-router";
import React from "react";
import { Image, Text, TouchableOpacity, View } from "react-native";
export interface ItemProps {
  id: string;
  name: string;
  price: number;
  color: string;
  image: string;
}
export default function Product({ item }: { item: ItemProps }) {
  function handleAddToCart() {
    console.log("Button clicked");
  }
  return (
    <View className="w-[95%] overflow-hidden  rounded-2xl border border-gray-300 pb-2 mr-2">
      <Link
        href={{
          pathname: "/(tabs)/products/[id]",
          params: {
            id: item.id,
            color: item.color,
          },
        }}
        className="overflow-hidden  rounded-2xl "
      >
        <Image
          className=" w-full h-32 object-cover"
          source={{ uri: item.image }}
        />
      </Link>
      <View className="px-3 pb-2">
        <View>
          <Text className="py-2">{item.name}</Text>
        </View>
        <View className="flex flex-row items-center justify-between">
          <Text className="font-bold">${item.price}</Text>
          <TouchableOpacity onPress={handleAddToCart} className="">
            <Text className="font-bold">Add</Text>
          </TouchableOpacity>
        </View>
      </View>
    </View>
  );
}

CART PAGE

import { CartScreen } from "@/premium-apps/ecommerce-app/screens/CartScreen";
import { useRouter } from "expo-router";
import React from "react";

export default function Cart() {
  const router = useRouter();

  return (
    <CartScreen
      onBackPress={() => {
        router.back();
      }}
      onCheckout={() => {
        console.log("Proceed to checkout");
        router.push("/checkout");
      }}
    />
  );
}

CART SCREEEN COMPONENT

// premium-apps/ecommerce-app/screens/CartScreen.tsx
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import {
  SafeAreaView,
  ScrollView,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from "react-native";
import { CartItemCard } from "../components/ui/CartItemCard";
import { CartItem, PromoCode } from "../types";

// Mock cart data
const mockCartItems: CartItem[] = [
  {
    id: "1",
    product: {
      id: "1",
      name: "Xbox series X",
      brand: "Microsoft",
      price: 570.0,
      rating: 4.8,
      reviewCount: 117,
      category: "Gaming",
      images: [
        "https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?w=300&h=300&fit=crop",
      ],
      description: "Next-gen gaming console",
      specifications: [],
      variants: [{ id: "1", name: "Storage", value: "1 TB", inStock: true }],
      inStock: true,
    },
    variant: { id: "1", name: "Storage", value: "1 TB", inStock: true },
    quantity: 1,
    addedAt: new Date(),
  },
  {
    id: "2",
    product: {
      id: "2",
      name: "Wireless Controller",
      brand: "Microsoft",
      price: 77.0,
      rating: 4.6,
      reviewCount: 89,
      category: "Gaming",
      images: [
        "https://img.freepik.com/free-photo/view-3d-video-game-controller_23-2151005826.jpg?ga=GA1.1.852679718.1745466476&semt=ais_hybrid&w=740",
      ],
      description: "Xbox wireless controller",
      specifications: [],
      variants: [{ id: "1", name: "Color", value: "Blue", inStock: true }],
      inStock: true,
    },
    variant: { id: "1", name: "Color", value: "Blue", inStock: true },
    quantity: 1,
    addedAt: new Date(),
  },
  {
    id: "3",
    product: {
      id: "3",
      name: "Razer Kaira Pro",
      brand: "Razer",
      price: 153.0,
      rating: 4.7,
      reviewCount: 156,
      category: "Gaming",
      images: [
        "https://images.unsplash.com/photo-1599669454699-248893623440?w=300&h=300&fit=crop",
      ],
      description: "Gaming headset with surround sound",
      specifications: [],
      variants: [{ id: "1", name: "Color", value: "Green", inStock: true }],
      inStock: true,
    },
    variant: { id: "1", name: "Color", value: "Green", inStock: true },
    quantity: 1,
    addedAt: new Date(),
  },
];

interface CartScreenProps {
  onBackPress: () => void;
  onCheckout: () => void;
}

export const CartScreen: React.FC<CartScreenProps> = ({
  onBackPress,
  onCheckout,
}) => {
  const [cartItems, setCartItems] = useState(mockCartItems);
  const [promoCode, setPromoCode] = useState("");
  const [appliedPromo, setAppliedPromo] = useState<PromoCode | null>({
    code: "ADJ3AK",
    discount: 40,
    type: "percentage",
    isApplied: true,
  });

  const updateQuantity = (id: string, quantity: number) => {
    setCartItems((prev) =>
      prev.map((item) => (item.id === id ? { ...item, quantity } : item))
    );
  };

  const removeItem = (id: string) => {
    setCartItems((prev) => prev.filter((item) => item.id !== id));
  };

  const applyPromoCode = () => {
    if (promoCode.trim()) {
      // Mock promo code validation
      const newPromo: PromoCode = {
        code: promoCode,
        discount: 20,
        type: "percentage",
        isApplied: true,
      };
      setAppliedPromo(newPromo);
      setPromoCode("");
    }
  };

  const subtotal = cartItems.reduce(
    (sum, item) => sum + item.product.price * item.quantity,
    0
  );
  const deliveryFee = 5.0;
  const discount = appliedPromo ? (subtotal * appliedPromo.discount) / 100 : 0;
  const total = subtotal + deliveryFee - discount;

  if (cartItems.length === 0) {
    return (
      <View className="flex-1 bg-gray-50">
        <SafeAreaView className="flex-1">
          <View className="flex-row items-center px-4 py-3 bg-white border-b border-gray-100">
            <TouchableOpacity onPress={onBackPress}>
              <Ionicons name="arrow-back" size={24} color="#000000" />
            </TouchableOpacity>
            <Text className="text-gray-900 text-xl font-bold ml-4">
              My cart
            </Text>
          </View>

          <View className="flex-1 items-center justify-center px-6">
            <View className="w-24 h-24 bg-gray-200 rounded-full items-center justify-center mb-6">
              <Ionicons name="bag-outline" size={40} color="#9ca3af" />
            </View>

            <Text className="text-gray-900 text-xl font-bold mb-2">
              Your cart is empty
            </Text>
            <Text className="text-gray-500 text-center mb-8">
              Add some products to get started with your order.
            </Text>

            <TouchableOpacity
              onPress={onBackPress}
              className="bg-green-500 px-8 py-4 rounded-2xl"
            >
              <Text className="text-white font-semibold text-lg">
                Continue Shopping
              </Text>
            </TouchableOpacity>
          </View>
        </SafeAreaView>
      </View>
    );
  }

  return (
    <View className="flex-1 bg-gray-50">
      <SafeAreaView className="flex-1">
        {/* Header */}
        <View className="flex-row items-center justify-between px-4 py-3 bg-white border-b border-gray-100 mt-10">
          <View className="flex-row items-center">
            <TouchableOpacity onPress={onBackPress}>
              <Ionicons name="arrow-back" size={24} color="#000000" />
            </TouchableOpacity>
            <Text className="text-gray-900 text-xl font-bold ml-4">
              My cart
            </Text>
          </View>

          <TouchableOpacity>
            <Ionicons name="ellipsis-vertical" size={24} color="#000000" />
          </TouchableOpacity>
        </View>

        <ScrollView
          className="flex-1 px-4 py-4"
          showsVerticalScrollIndicator={false}
        >
          {/* Cart Items */}
          {cartItems.map((item) => (
            <CartItemCard
              key={item.id}
              item={item}
              onUpdateQuantity={updateQuantity}
              onRemove={removeItem}
            />
          ))}

          {/* Promo Code */}
          <View className="bg-white rounded-2xl p-4 mb-4">
            {appliedPromo ? (
              <View className="flex-row items-center justify-between">
                <View className="flex-1">
                  <Text className="text-gray-900 font-semibold text-base mb-1">
                    {appliedPromo.code}
                  </Text>
                  <View className="flex-row items-center">
                    <Text className="text-green-500 font-medium mr-2">
                      Promocode applied
                    </Text>
                    <Ionicons
                      name="checkmark-circle"
                      size={16}
                      color="#10b981"
                    />
                  </View>
                </View>

                <TouchableOpacity
                  onPress={() => setAppliedPromo(null)}
                  className="p-2"
                >
                  <Ionicons name="close" size={20} color="#ef4444" />
                </TouchableOpacity>
              </View>
            ) : (
              <View>
                <Text className="text-gray-900 font-semibold text-base mb-3">
                  Promo Code
                </Text>

                <View className="flex-row">
                  <TextInput
                    value={promoCode}
                    onChangeText={setPromoCode}
                    placeholder="Enter promo code"
                    className="flex-1 bg-gray-100 rounded-l-xl px-4 py-3 text-gray-900"
                    placeholderTextColor="#9ca3af"
                  />

                  <TouchableOpacity
                    onPress={applyPromoCode}
                    className="bg-green-500 px-6 rounded-r-xl items-center justify-center"
                  >
                    <Text className="text-white font-semibold">Apply</Text>
                  </TouchableOpacity>
                </View>
              </View>
            )}
          </View>

          {/* Order Summary */}
          <View className="bg-white rounded-2xl p-4 mb-4">
            <Text className="text-gray-900 font-semibold text-lg mb-4">
              Order Summary
            </Text>

            <View className="space-y-3">
              <View className="flex-row justify-between">
                <Text className="text-gray-600">Subtotal:</Text>
                <Text className="text-gray-900 font-semibold">
                  ${subtotal.toFixed(2)}
                </Text>
              </View>

              <View className="flex-row justify-between">
                <Text className="text-gray-600">Delivery Fee:</Text>
                <Text className="text-gray-900 font-semibold">
                  ${deliveryFee.toFixed(2)}
                </Text>
              </View>

              {appliedPromo && (
                <View className="flex-row justify-between">
                  <Text className="text-gray-600">Discount:</Text>
                  <Text className="text-green-500 font-semibold">
                    -{appliedPromo.discount}%
                  </Text>
                </View>
              )}

              <View className="h-px bg-gray-200" />

              <View className="flex-row justify-between">
                <Text className="text-gray-900 font-bold text-lg">Total:</Text>
                <Text className="text-gray-900 font-bold text-lg">
                  ${total.toFixed(2)}
                </Text>
              </View>
            </View>
          </View>
        </ScrollView>

        {/* Checkout Button */}
        <View className="px-4 pb-6 bg-white border-t border-gray-100">
          <TouchableOpacity
            onPress={onCheckout}
            className="bg-green-500 rounded-2xl py-4 items-center"
          >
            <Text className="text-white text-lg font-semibold">
              Checkout for ${total.toFixed(2)}
            </Text>
          </TouchableOpacity>
        </View>
      </SafeAreaView>
    </View>
  );
};

CART CARD COMPONENT

import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { Image, Text, TouchableOpacity, View } from "react-native";
import { CartItem } from "../../types";

interface CartItemCardProps {
  item: CartItem;
  onUpdateQuantity: (id: string, quantity: number) => void;
  onRemove: (id: string) => void;
}

export const CartItemCard: React.FC<CartItemCardProps> = ({
  item,
  onUpdateQuantity,
  onRemove,
}) => {
  const increaseQuantity = () => {
    onUpdateQuantity(item.id, item.quantity + 1);
  };

  const decreaseQuantity = () => {
    if (item.quantity > 1) {
      onUpdateQuantity(item.id, item.quantity - 1);
    }
  };

  return (
    <View className="flex-row bg-white rounded-2xl p-4 mb-3 border border-gray-100">
      <Image
        source={{ uri: item.product.images[0] }}
        className="w-20 h-20 rounded-xl bg-gray-50"
        resizeMode="contain"
      />

      <View className="flex-1 ml-4">
        <View className="flex-row justify-between items-start mb-2">
          <View className="flex-1 pr-2">
            <Text className="text-gray-900 font-semibold text-base mb-1">
              {item.product.name}
            </Text>
            {item.variant && (
              <Text className="text-gray-500 text-sm">
                {item.variant.name}: {item.variant.value}
              </Text>
            )}
          </View>

          <TouchableOpacity onPress={() => onRemove(item.id)} className="p-1">
            <Ionicons name="close" size={20} color="#ef4444" />
          </TouchableOpacity>
        </View>

        <View className="flex-row justify-between items-center">
          <Text className="text-gray-900 font-bold text-lg">
            ${(item.product.price * item.quantity).toFixed(2)}
          </Text>

          <View className="flex-row items-center">
            <TouchableOpacity
              onPress={decreaseQuantity}
              className="w-8 h-8 rounded-full bg-gray-100 items-center justify-center"
            >
              <Ionicons name="remove" size={16} color="#6b7280" />
            </TouchableOpacity>

            <Text className="mx-4 text-gray-900 font-semibold text-base">
              {item.quantity}
            </Text>

            <TouchableOpacity
              onPress={increaseQuantity}
              className="w-8 h-8 rounded-full bg-green-500 items-center justify-center"
            >
              <Ionicons name="add" size={16} color="#ffffff" />
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </View>
  );
};

Subscribe to My Latest Guides

All the latest Guides and tutorials, straight from the team.

Subscribe to be the first to receive the new Guide when it comes out

•Get All Guides at Once