React Native Mobile App Development: From Expo to Production
A comprehensive guide to building production-ready mobile applications with React Native, Expo, and Supabase. Learn best practices for authentication, state management, and deployment.
Vaibhav Parmar
Full Stack Developer
Introduction
Mobile app development has evolved significantly, and React Native has become one of the most popular frameworks for building cross-platform applications. In this guide, I'll share my experience building the Turf Cricket booking platform and other mobile apps using React Native, Expo, and Supabase.
Why React Native + Expo?
Cross-Platform Development
React Native allows you to write once and deploy to both iOS and Android, significantly reducing development time and maintenance overhead.
Expo for Rapid Development
Expo provides:
- Expo Router: File-based routing system
- Expo SDK: Access to native device features
- Over-the-Air Updates: Deploy updates without app store approval
- Development Tools: Excellent debugging and testing tools
Supabase for Backend
Supabase offers a complete backend solution perfect for mobile apps:
- Real-time subscriptions
- Offline-first architecture
- Built-in authentication
- Row Level Security
Project Setup and Architecture
Initial Setup
# Create new Expo project
npx create-expo-app@latest TurfCricket --template
cd TurfCricket
# Install dependencies
npm install @supabase/supabase-js
npm install @react-navigation/native @react-navigation/stack
npm install expo-router expo-linking expo-constants
Project Structure
src/
├── app/
│ ├── (auth)/
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── (tabs)/
│ │ ├── home.tsx
│ │ ├── bookings.tsx
│ │ └── profile.tsx
│ └── _layout.tsx
├── components/
│ ├── ui/
│ ├── forms/
│ └── booking/
├── lib/
│ ├── supabase.ts
│ ├── auth.ts
│ └── utils.ts
└── types/
└── index.ts
Authentication Implementation
Supabase Client Setup
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
Authentication Context
// lib/auth.ts
import { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '@supabase/supabase-js';
import { supabase } from './supabase';
interface AuthContextType {
user: User | null;
session: Session | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
State Management with TanStack Query
Query Client Setup
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
},
},
});
Custom Hooks for Data Fetching
// hooks/useBookings.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
export const useBookings = (userId: string) => {
return useQuery({
queryKey: ['bookings', userId],
queryFn: async () => {
const { data, error } = await supabase
.from('bookings')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
};
export const useCreateBooking = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (bookingData: BookingData) => {
const { data, error } = await supabase.from('bookings').insert([bookingData]).select();
if (error) throw error;
return data[0];
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bookings'] });
},
});
};
Real-time Features
Real-time Subscriptions
// hooks/useRealtimeBookings.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
export const useRealtimeBookings = (turfId: string) => {
const [bookings, setBookings] = useState([]);
useEffect(() => {
const channel = supabase
.channel('turf_bookings')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'bookings',
filter: `turf_id=eq.${turfId}`,
},
(payload) => {
if (payload.eventType === 'INSERT') {
setBookings((prev) => [...prev, payload.new]);
} else if (payload.eventType === 'UPDATE') {
setBookings((prev) =>
prev.map((booking) => (booking.id === payload.new.id ? payload.new : booking))
);
} else if (payload.eventType === 'DELETE') {
setBookings((prev) => prev.filter((booking) => booking.id !== payload.old.id));
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [turfId]);
return bookings;
};
UI/UX Best Practices
Responsive Design
// components/ResponsiveContainer.tsx
import { View, StyleSheet, Dimensions } from 'react-native'
const { width, height } = Dimensions.get('window')
export const ResponsiveContainer = ({ children, style }) => {
return (
<View style={[styles.container, style]}>
{children}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: width > 768 ? 24 : 16,
},
})
Custom Components
// components/ui/Button.tsx
import React from 'react'
import { TouchableOpacity, Text, StyleSheet } from 'react-native'
interface ButtonProps {
title: string
onPress: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
}
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
}) => {
return (
<TouchableOpacity
style={[
styles.button,
styles[variant],
disabled && styles.disabled,
]}
onPress={onPress}
disabled={disabled}
>
<Text style={[styles.text, styles[`${variant}Text`]]}>
{title}
</Text>
</TouchableOpacity>
)
}
Performance Optimization
Image Optimization
// components/OptimizedImage.tsx
import { Image } from 'expo-image'
export const OptimizedImage = ({ uri, ...props }) => {
return (
<Image
source={{ uri }}
style={props.style}
contentFit="cover"
transition={200}
placeholder="blur"
cachePolicy="memory-disk"
/>
)
}
Lazy Loading
// components/LazyList.tsx
import { FlatList } from 'react-native'
import { useMemo } from 'react'
export const LazyList = ({ data, renderItem, ...props }) => {
const memoizedData = useMemo(() => data, [data])
return (
<FlatList
data={memoizedData}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
{...props}
/>
)
}
Testing Strategy
Unit Testing with Jest
// __tests__/components/Button.test.tsx
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import { Button } from '@/components/ui/Button'
describe('Button Component', () => {
it('renders correctly', () => {
const { getByText } = render(
<Button title="Test Button" onPress={() => {}} />
)
expect(getByText('Test Button')).toBeTruthy()
})
it('calls onPress when pressed', () => {
const mockOnPress = jest.fn()
const { getByText } = render(
<Button title="Test Button" onPress={mockOnPress} />
)
fireEvent.press(getByText('Test Button'))
expect(mockOnPress).toHaveBeenCalledTimes(1)
})
})
Deployment and Distribution
EAS Build Configuration
// eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"ios": {
"simulator": true
}
},
"production": {
"ios": {
"autoIncrement": "buildNumber"
},
"android": {
"autoIncrement": "versionCode"
}
}
},
"submit": {
"production": {}
}
}
Environment Variables
// app.config.js
export default {
expo: {
name: 'Turf Cricket',
slug: 'turf-cricket',
version: '1.0.0',
extra: {
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL,
supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
},
},
};
Common Challenges and Solutions
1. Navigation State Management
Use React Navigation with proper state persistence and deep linking configuration.
2. Offline Support
Implement proper caching strategies and offline-first data synchronization.
3. Platform-Specific Code
Use Platform.select() and platform-specific files when needed.
4. Memory Management
Implement proper cleanup in useEffect hooks and avoid memory leaks.
Conclusion
React Native with Expo provides an excellent foundation for building production-ready mobile applications. The combination of modern web technologies with native performance makes it an ideal choice for cross-platform development.
Key takeaways:
- Start with Expo for rapid development
- Use Supabase for backend services
- Implement proper state management
- Focus on performance optimization
- Test thoroughly before deployment
This guide is based on my experience building the Turf Cricket mobile app. You can view the source code on GitHub and learn more about my mobile development projects.

Vaibhav Parmar
Software Engineer & Technical Writer
Passionate about building privacy-focused applications and seamless user experiences. I specialize in React.js, React Native, Next.js, and mobile development. Always exploring new tools and techniques to create better digital experiences.
Explore More Topics
Continue Reading
Discover more articles on similar topics that might interest you