DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on

Building a Production-Ready E-Commerce App with React Native & Expo

Look, I've spent the last few months building e-commerce apps, and honestly? Most tutorials out there are trash. They show you how to display products in a FlatList and call it a day. That's not how real apps work.

So I'm documenting my complete approach to building a proper e-commerce application that's actually ready for the App Store and Google Play. No shortcuts, no "we'll add this later" nonsense. Just pure, production-grade architecture.

Why Another E-Commerce Tutorial?

Because most of them suck. They don't talk about:

  • How to actually structure your codebase so you're not drowning in spaghetti code by week 2
  • Real state management that doesn't make you want to cry
  • Proper navigation patterns that users actually understand
  • Performance optimization (because nobody wants a laggy checkout)
  • How to handle offline mode (yes, people still lose internet connection)

This isn't a "follow along" tutorial. This is the architecture document I wish I had when I started.

The Tech Stack (and Why)

React Native with Expo - Hot take: Expo is no longer the "beginner framework." With EAS and custom dev clients, you get 90% of the benefits with 10% of the headache. Unless you're building a crypto wallet or some AR-heavy app, Expo is the move.

TypeScript - If you're still writing JavaScript in 2025, we need to talk. Type safety isn't optional anymore.

Zustand for State Management - Redux is overkill. Context API is underwhelming. Zustand is the Goldilocks solution. Simple, performant, and you can actually understand it after 3 months away from the code.

React Navigation - Because, well, there's no real alternative.

TanStack Query - Server state is NOT the same as client state. Stop putting API data in your global store. React Query handles caching, refetching, and background updates like a boss.

Stripe for Payments - Because rolling your own payment system is how you end up on the news.

File Structure That Won't Make You Hate Yourself

Here's the folder structure I'm using. It's opinionated, but it scales:

/src
  /api
    /services
      authService.ts
      productService.ts
      orderService.ts
    /hooks
      useProducts.ts
      useCart.ts
      useAuth.ts
    client.ts

  /components
    /common
      Button.tsx
      Input.tsx
      Card.tsx
      LoadingSpinner.tsx
    /product
      ProductCard.tsx
      ProductGrid.tsx
      ProductDetail.tsx
    /cart
      CartItem.tsx
      CartSummary.tsx
    /checkout
      CheckoutForm.tsx
      PaymentSheet.tsx

  /screens
    /auth
      LoginScreen.tsx
      RegisterScreen.tsx
    /home
      HomeScreen.tsx
    /product
      ProductListScreen.tsx
      ProductDetailScreen.tsx
    /cart
      CartScreen.tsx
    /checkout
      CheckoutScreen.tsx
    /profile
      ProfileScreen.tsx
      OrderHistoryScreen.tsx

  /navigation
    RootNavigator.tsx
    AuthNavigator.tsx
    MainNavigator.tsx
    types.ts

  /store
    useAuthStore.ts
    useCartStore.ts
    useThemeStore.ts

  /utils
    validation.ts
    formatting.ts
    constants.ts

  /types
    product.types.ts
    user.types.ts
    order.types.ts

  /assets
    /images
    /fonts
    /icons

/app.json
/babel.config.js
/tsconfig.json
Enter fullscreen mode Exit fullscreen mode

The Key Principle: Feature-Based + Layer-Based Hybrid

Notice how I'm not going full feature-based (where everything for "products" lives in one folder) or full layer-based (where all components live together). It's a hybrid.

Why? Because in e-commerce, some things are truly shared (Button, Input), while others are feature-specific (ProductCard). This structure respects both realities.

System Architecture: The Real Stuff

Here's how the data flows:

Authentication Layer

User Input → useAuthStore → authService → Backend
              ↓
         Zustand Store (token)
              ↓
         Persisted Storage
              ↓
         API Client (auto-inject token)
Enter fullscreen mode Exit fullscreen mode

Every API call automatically includes the auth token. If a 401 comes back, we auto-logout. No manual token management in components.

Product Discovery Flow

HomeScreen → useProducts hook → TanStack Query
                ↓
          productService.ts
                ↓
          API Client → Backend
                ↓
          Cached in React Query
                ↓
          Background Refetch (stale-while-revalidate)
Enter fullscreen mode Exit fullscreen mode

Products are cached aggressively. Stale data is served instantly, fresh data loads in the background. Users never wait.

Cart Management

User Action → useCartStore (Zustand)
                ↓
          Optimistic Update (instant UI)
                ↓
          Async Sync to Backend
                ↓
          Rollback on Failure
Enter fullscreen mode Exit fullscreen mode

The cart feels instant because we're optimistic. Add to cart? It's there immediately. Backend call fails? We roll it back with a toast notification.

Checkout Flow

CartScreen → CheckoutScreen → Payment Intent
                                    ↓
                              Stripe SDK
                                    ↓
                              Confirmation
                                    ↓
                              Order Creation
                                    ↓
                              Clear Cart
                                    ↓
                              Order History
Enter fullscreen mode Exit fullscreen mode

Nothing enters the order history until payment succeeds. Cart clearing is an atomic operation. No weird states.

The UI/UX Philosophy

I'm going for what I call "Invisible Excellence." The UI should be so intuitive that users never think about it.

Core Design Principles:

1. Minimalist Product Cards
No borders, no shadows, no nonsense. Just a great product photo, title, price. Let the products speak.

2. Bottom Sheet Everything
Filters? Bottom sheet. Product options? Bottom sheet. Users expect this pattern now. Don't fight it.

3. Gesture-First Navigation
Swipe to go back. Pull to refresh. Long press for quick actions. Make it feel native.

4. Skeleton Screens > Spinners
Loading states should look like the content they're replacing. Spinners are so 2018.

5. Micro-Interactions Matter
Button press animations. Cart icon bounce when adding items. Subtle haptic feedback. These details separate good apps from great apps.

Performance: The Stuff Nobody Talks About

Image Optimization

// Bad
<Image source={{ uri: product.image }} />

// Good
<Image 
  source={{ uri: product.image }}
  placeholder={blurhash}
  contentFit="cover"
  transition={200}
  cachePolicy="memory-disk"
/>
Enter fullscreen mode Exit fullscreen mode

Using Expo Image with blurhash placeholders. Images load progressively, cache aggressively, and never cause jank.

List Performance

<FlashList
  data={products}
  estimatedItemSize={200}
  renderItem={({ item }) => <ProductCard product={item} />}
  removeClippedSubviews
  maxToRenderPerBatch={10}
/>
Enter fullscreen mode Exit fullscreen mode

FlashList over FlatList. 60fps scrolling is non-negotiable.

Code Splitting

const CheckoutScreen = lazy(() => import('./screens/checkout/CheckoutScreen'));
Enter fullscreen mode Exit fullscreen mode

Payment SDK is 2MB. Don't load it until checkout. Bundle size matters.

State Management Strategy

Here's the controversial part: I use THREE state management solutions, and it's perfect.

Zustand: Client state (cart, auth, preferences)
TanStack Query: Server state (products, orders)
React Context: Theme and i18n only

Each tool does what it's best at. Stop trying to make one solution handle everything.

Offline Support

E-commerce apps need to work offline. At least partially.

// Cached product browsing
const { data: products } = useProducts({
  staleTime: 1000 * 60 * 60, // 1 hour
  cacheTime: 1000 * 60 * 60 * 24, // 24 hours
});

// Queue cart updates
const addToCart = useCartStore(state => state.addItem);
// Automatically syncs when connection restores
Enter fullscreen mode Exit fullscreen mode

Users can browse previously viewed products offline. Cart changes queue up and sync when online. Orders require connection (obviously).

Navigation Architecture

// Stack-based with modal support
Root Navigator (Stack)
  ├── Auth Navigator (Stack)
     ├── Login
     └── Register
  └── Main Navigator (Tabs)
      ├── Home (Stack)
      ├── Search (Stack)
      ├── Cart (Stack)
      └── Profile (Stack)
Enter fullscreen mode Exit fullscreen mode

Deep linking is configured for every screen. Push notifications? They work. Product share links? They open the right screen. Universal links? Handled.

Security Checklist

  • ✅ Tokens in secure storage (not AsyncStorage)
  • ✅ Certificate pinning for API calls
  • ✅ No sensitive data in Redux DevTools
  • ✅ Payment data never touches our servers
  • ✅ Biometric authentication for checkout
  • ✅ Session timeout on background

Testing Strategy

Unit Tests: Pure functions and utilities
Integration Tests: API services with MSW
E2E Tests: Critical paths with Detox (login, purchase)

I don't test UI components. Fight me. They change too fast, and the ROI isn't there.

Deployment Pipeline

Git Push → GitHub Actions
           ↓
       Run Tests
           ↓
       EAS Build (iOS + Android)
           ↓
       Upload to TestFlight + Play Internal
           ↓
       Automated E2E Tests
           ↓
       Staging Deployment
           ↓
       Manual Approval
           ↓
       Production Release
Enter fullscreen mode Exit fullscreen mode

One command. Fifteen minutes later, it's in TestFlight. Zero manual steps.

The Mistakes I Made (So You Don't Have To)

1. Started without TypeScript
Added it later. That migration took 3 weeks. Just start with it.

2. Put everything in Redux
Loading states, API data, user preferences, cart, all in one store. Debugging was hell.

3. Ignored accessibility
Submitted to App Store. Got rejected. Added proper labels. Submitted again. Approved.

4. Over-engineered early
Built a complex theming system before having a single screen working. YAGNI is real.

5. Didn't set up error tracking immediately
Production bugs happened. Had no idea what users were experiencing. Added Sentry day one now.

What's Next?

This architecture is just the foundation. Here's what comes next:

  • Push notifications for order updates
  • Wishlists with sync across devices
  • Product recommendations (actual ML, not random)
  • AR try-on for applicable products
  • Social sharing with dynamic OG images
  • Subscription model for recurring products

The Reality Check

Building a production e-commerce app isn't a weekend project. This architecture took months to refine. But once it's set up? Adding new features is fast. Onboarding developers is easy. The app is stable.

Most importantly: users don't complain. They just buy stuff and leave good reviews.

That's the goal.

Resources That Actually Helped

  • React Native EU talks (skip the intro ones)
  • Shopify's mobile engineering blog
  • Stripe's React Native SDK docs
  • Expo's EAS documentation
  • TanStack Query docs (seriously, read them)

No courses. No paid content. Just official docs and conference talks from people building real apps.

Final Thoughts

You don't need a perfect architecture to start. But you need a plan. This is mine. Take what works, ignore what doesn't, adapt to your needs.

The best code is code that ships. But shipped code that's maintainable? That's the dream.

Now stop reading and start building. The App Store isn't going to submit to itself.


That's a wrap 🎁

Now go touch some code 👨‍💻

Catch me here → LinkedIn | GitHub | YouTube

Top comments (0)