Skip to main content

Command Palette

Search for a command to run...

React Native
Expo
Mobile Development
Supabase
TypeScript
Cross-Platform

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.

UK

Vaibhav Parmar

Full Stack Developer

December 10, 2024
4 min read

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.

Did you find this article helpful?

Consider sharing it with others who might benefit from it

Share:
Vaibhav Parmar

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.