Skip to main content
This tutorial was written by Claude Code (an AI) and has not yet been reviewed. Follow along with caution. If the tutorial was helpful or a specific part was not clear/correct, please provide feedback at the bottom of the page. Thank you.

Introduction

React Native is a popular framework for building native mobile applications using React. It’s an excellent choice for creating mobile storefronts with CoCart because it allows you to build iOS and Android apps from a single codebase while maintaining native performance and user experience. This guide will walk you through setting up a React Native project configured to work with CoCart API.

Why React Native for Headless Commerce?

  • Cross-platform - Build for iOS and Android from one codebase
  • Native performance - Real native components, not webviews
  • React ecosystem - Use familiar React patterns and libraries
  • Hot reloading - Fast development with instant feedback
  • Large community - Extensive third-party packages and support
  • Cost effective - One team can build for multiple platforms

Prerequisites

  • Node.js 18 or higher
  • A WordPress site with WooCommerce installed
  • CoCart plugin installed and activated
  • Basic knowledge of React and JavaScript
  • For iOS development: macOS with Xcode installed
  • For Android development: Android Studio installed

Development Environment Setup

Before creating your project, set up your development environment:
# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install Node and Watchman
brew install node
brew install watchman

# Install CocoaPods (for iOS dependencies)
sudo gem install cocoapods
For detailed environment setup, refer to React Native Environment Setup.

Creating a New React Native Project

Create a new React Native project using the official CLI:
npx react-native@latest init MyHeadlessStore
Navigate to your project:
cd MyHeadlessStore

Project Structure

Your React Native project will have this structure:
MyHeadlessStore/
├── android/           # Android native code
├── ios/              # iOS native code
├── src/              # Your app code (create this)
│   ├── api/          # API client
│   ├── components/   # Reusable components
│   ├── screens/      # Screen components
│   ├── navigation/   # Navigation setup
│   ├── context/      # Context providers
│   ├── hooks/        # Custom hooks
│   └── types/        # TypeScript types
├── App.tsx           # Root component
├── package.json
└── tsconfig.json
Create the source directory structure:
mkdir -p src/api src/components src/screens src/navigation src/context src/hooks src/types

Installing Essential Dependencies

Install the necessary packages for a complete e-commerce app:
# Navigation
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs

# Navigation dependencies
npm install react-native-screens react-native-safe-area-context

# State management and data fetching
npm install @tanstack/react-query zustand

# Async storage for cart persistence
npm install @react-native-async-storage/async-storage

# Image handling
npm install react-native-fast-image

# Additional utilities
npm install axios
For iOS, install pods:
cd ios && pod install && cd ..

Environment Configuration

Install the config package:
npm install react-native-config
Create a .env file in your project root:
API_URL=https://yourstore.com/wp-json/cocart/v2
STORE_URL=https://yourstore.com
Add .env to your .gitignore:
echo ".env" >> .gitignore
Create a .env.example:
# .env.example
API_URL=https://yourstore.com/wp-json/cocart/v2
STORE_URL=https://yourstore.com

Creating the CoCart API Client

Create a centralized API client at src/api/cocart.ts:
We are currently building out this client, so for now just make standard fetch/axios requests to the CoCart API endpoints as needed.
import axios, { AxiosInstance } from 'axios';
import Config from 'react-native-config';
import AsyncStorage from '@react-native-async-storage/async-storage';

const API_BASE = Config.API_URL || 'https://yourstore.com/wp-json/cocart/v2';

// Create axios instance
const api: AxiosInstance = axios.create({
  baseURL: API_BASE,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add cart key to requests
api.interceptors.request.use(
  async (config) => {
    const cartKey = await AsyncStorage.getItem('cart_key');
    if (cartKey && config.headers) {
      config.headers['Cart-Key'] = cartKey;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Save cart key from responses
api.interceptors.response.use(
  async (response) => {
    const cartKey = response.headers['cart-key'];
    if (cartKey) {
      await AsyncStorage.setItem('cart_key', cartKey);
    }
    return response;
  },
  (error) => Promise.reject(error)
);

/**
 * Fetch products from CoCart API
 */
export async function getProducts(params: {
  per_page?: number;
  page?: number;
  category?: string;
  search?: string;
  [key: string]: any;
} = {}) {
  try {
    const response = await api.get('/products', { params });
    return response.data;
  } catch (error) {
    console.error('Error fetching products:', error);
    throw error;
  }
}

/**
 * Get a single product by ID
 */
export async function getProduct(productId: string | number) {
  try {
    const response = await api.get(`/products/${productId}`);
    return response.data;
  } catch (error) {
    console.error('Error fetching product:', error);
    throw error;
  }
}

/**
 * Get current cart
 */
export async function getCart() {
  try {
    const response = await api.get('/cart');
    return response.data;
  } catch (error) {
    console.error('Error fetching cart:', error);
    throw error;
  }
}

/**
 * Add item to cart
 */
export async function addToCart(
  productId: string,
  quantity: number = 1,
  options: Record<string, any> = {}
) {
  try {
    const response = await api.post('/cart/add-item', {
      id: productId,
      quantity,
      ...options,
    });
    return response.data;
  } catch (error) {
    console.error('Error adding to cart:', error);
    throw error;
  }
}

/**
 * Update cart item quantity
 */
export async function updateCartItem(itemKey: string, quantity: number) {
  try {
    const response = await api.post(`/cart/item/${itemKey}`, { quantity });
    return response.data;
  } catch (error) {
    console.error('Error updating cart item:', error);
    throw error;
  }
}

/**
 * Remove item from cart
 */
export async function removeCartItem(itemKey: string) {
  try {
    const response = await api.delete(`/cart/item/${itemKey}`);
    return response.data;
  } catch (error) {
    console.error('Error removing cart item:', error);
    throw error;
  }
}

/**
 * Clear cart
 */
export async function clearCart() {
  try {
    const response = await api.post('/cart/clear');
    await AsyncStorage.removeItem('cart_key');
    return response.data;
  } catch (error) {
    console.error('Error clearing cart:', error);
    throw error;
  }
}

export default api;

Setting Up React Query

Create a query client configuration at src/api/queryClient.ts:
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      cacheTime: 1000 * 60 * 10, // 10 minutes
      retry: 2,
    },
  },
});

Creating Custom Hooks

Create a cart hook at src/hooks/useCart.ts:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getCart, addToCart, updateCartItem, removeCartItem, clearCart } from '../api/cocart';
import { Alert } from 'react-native';

export function useCart() {
  const queryClient = useQueryClient();

  // Get cart query
  const { data: cart, isLoading, error } = useQuery({
    queryKey: ['cart'],
    queryFn: getCart,
  });

  // Add to cart mutation
  const addToCartMutation = useMutation({
    mutationFn: ({ productId, quantity, options }: any) =>
      addToCart(productId, quantity, options),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
      Alert.alert('Success', 'Item added to cart');
    },
    onError: (error: any) => {
      Alert.alert('Error', error.message || 'Failed to add item to cart');
    },
  });

  // Update cart item mutation
  const updateCartMutation = useMutation({
    mutationFn: ({ itemKey, quantity }: any) => updateCartItem(itemKey, quantity),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
    onError: (error: any) => {
      Alert.alert('Error', error.message || 'Failed to update cart');
    },
  });

  // Remove cart item mutation
  const removeCartMutation = useMutation({
    mutationFn: (itemKey: string) => removeCartItem(itemKey),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
    onError: (error: any) => {
      Alert.alert('Error', error.message || 'Failed to remove item');
    },
  });

  // Clear cart mutation
  const clearCartMutation = useMutation({
    mutationFn: clearCart,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
    onError: (error: any) => {
      Alert.alert('Error', error.message || 'Failed to clear cart');
    },
  });

  return {
    cart,
    isLoading,
    error,
    addToCart: addToCartMutation.mutate,
    updateCartItem: updateCartMutation.mutate,
    removeCartItem: removeCartMutation.mutate,
    clearCart: clearCartMutation.mutate,
    isAddingToCart: addToCartMutation.isPending,
  };
}
Create a products hook at src/hooks/useProducts.ts:
import { useQuery } from '@tanstack/react-query';
import { getProducts, getProduct } from '../api/cocart';

export function useProducts(params?: any) {
  return useQuery({
    queryKey: ['products', params],
    queryFn: () => getProducts(params),
  });
}

export function useProduct(productId: string | number) {
  return useQuery({
    queryKey: ['product', productId],
    queryFn: () => getProduct(productId),
    enabled: !!productId,
  });
}

Setting Up Navigation

Create the navigation structure at src/navigation/AppNavigator.tsx:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

// Import screens (we'll create these next)
import HomeScreen from '../screens/HomeScreen';
import ProductDetailScreen from '../screens/ProductDetailScreen';
import CartScreen from '../screens/CartScreen';
import ProfileScreen from '../screens/ProfileScreen';

const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();

function HomeTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Cart" component={CartScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

export default function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="HomeTabs"
          component={HomeTabs}
          options={{ headerShown: false }}
        />
        <Stack.Screen
          name="ProductDetail"
          component={ProductDetailScreen}
          options={{ title: 'Product Details' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Creating Example Screens

Create a basic home screen at src/screens/HomeScreen.tsx:
import React from 'react';
import {
  View,
  Text,
  FlatList,
  TouchableOpacity,
  StyleSheet,
  ActivityIndicator,
  Image,
} from 'react-native';
import { useProducts } from '../hooks/useProducts';
import FastImage from 'react-native-fast-image';

export default function HomeScreen({ navigation }: any) {
  const { data: products, isLoading, error } = useProducts({ per_page: 20 });

  if (isLoading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#6a42d7" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text style={styles.errorText}>Error loading products</Text>
      </View>
    );
  }

  const renderProduct = ({ item }: any) => (
    <TouchableOpacity
      style={styles.productCard}
      onPress={() => navigation.navigate('ProductDetail', { productId: item.id })}
    >
      {item.images?.[0]?.src && (
        <FastImage
          source={{ uri: item.images[0].src }}
          style={styles.productImage}
          resizeMode={FastImage.resizeMode.cover}
        />
      )}
      <View style={styles.productInfo}>
        <Text style={styles.productName} numberOfLines={2}>
          {item.name}
        </Text>
        <Text style={styles.productPrice}>
          {item.prices.currency_symbol}{item.prices.price}
        </Text>
      </View>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <FlatList
        data={products}
        renderItem={renderProduct}
        keyExtractor={(item) => item.id.toString()}
        numColumns={2}
        contentContainerStyle={styles.list}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  list: {
    padding: 8,
  },
  productCard: {
    flex: 1,
    margin: 8,
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
    overflow: 'hidden',
  },
  productImage: {
    width: '100%',
    height: 150,
    backgroundColor: '#f5f5f5',
  },
  productInfo: {
    padding: 12,
  },
  productName: {
    fontSize: 14,
    fontWeight: '600',
    marginBottom: 4,
    color: '#333',
  },
  productPrice: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#6a42d7',
  },
  errorText: {
    fontSize: 16,
    color: '#ff0000',
  },
});
Create a placeholder for the cart screen at src/screens/CartScreen.tsx:
import React from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator } from 'react-native';
import { useCart } from '../hooks/useCart';

export default function CartScreen() {
  const { cart, isLoading } = useCart();

  if (isLoading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#6a42d7" />
      </View>
    );
  }

  if (!cart || cart.items_count === 0) {
    return (
      <View style={styles.centered}>
        <Text style={styles.emptyText}>Your cart is empty</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Cart ({cart.items_count} items)</Text>
      <Text style={styles.total}>
        Total: {cart.totals.currency_symbol}{cart.totals.total}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#fff',
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  total: {
    fontSize: 18,
    fontWeight: '600',
    color: '#6a42d7',
  },
  emptyText: {
    fontSize: 16,
    color: '#999',
  },
});
Create placeholder screens:
// src/screens/ProductDetailScreen.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export default function ProductDetailScreen() {
  return (
    <View style={styles.container}>
      <Text>Product Detail Screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});
// src/screens/ProfileScreen.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export default function ProfileScreen() {
  return (
    <View style={styles.container}>
      <Text>Profile Screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});

Updating App.tsx

Update your root App.tsx file:
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './src/api/queryClient';
import AppNavigator from './src/navigation/AppNavigator';

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppNavigator />
    </QueryClientProvider>
  );
}

Running Your App

npm run ios

# Or for a specific simulator
npm run ios -- --simulator="iPhone 15"

Testing on Physical Devices

1. Open ios/MyHeadlessStore.xcworkspace in Xcode
2. Select your device from the device menu
3. Click Run button

Building for Production

cd ios
pod install
cd ..

# Build for release
npx react-native run-ios --configuration Release

App Store Submission

1. Open ios/MyHeadlessStore.xcworkspace in Xcode
2. Select "Any iOS Device" as target
3. Product → Archive
4. Upload to App Store Connect

Performance Optimization

Image Optimization

  • Use react-native-fast-image for better image performance
  • Cache images appropriately
  • Use appropriate image sizes

List Optimization

  • Use FlatList with proper keyExtractor
  • Implement getItemLayout for fixed-height items
  • Use maxToRenderPerBatch and windowSize props

Code Splitting

npm install @react-native-community/cli-plugin-metro

Troubleshooting

Metro Bundler Issues

# Clear cache and restart
npm start -- --reset-cache
cd ios
pod deintegrate
pod install
cd ..

CORS Errors

If you encounter CORS errors, configure WordPress to allow cross-origin requests. See CORS documentation.

Network Issues

Use localhost or 127.0.0.1 in your API_URL:
API_URL=http://localhost:8000/wp-json/cocart/v2

Next Steps

Now that your React Native app is set up with CoCart:
  1. Implement full product detail screens
  2. Complete cart functionality (update quantities, remove items)
  3. Add checkout flow
  4. Implement user authentication with JWT
  5. Add push notifications for order updates
  6. Implement offline support with AsyncStorage
  7. Add analytics and crash reporting

Resources

I