Implementation Guide

Mobile App Development with React Native

Build production-quality iOS and Android apps from a single TypeScript codebase — covering navigation, state management, native APIs, and App Store submission.

40 min read
Intermediate
Updated 2025
React Native Mobile iOS Android
1

React Native Overview

React Native renders real native UI components — not WebViews. Your JavaScript code runs in a separate thread and communicates with the native side via the New Architecture's JSI (JavaScript Interface), which replaced the legacy asynchronous bridge with synchronous, direct memory access.

Expo (Managed Workflow)
  • Zero native toolchain setup
  • OTA updates via Expo Updates
  • Pre-built native modules
  • EAS Build for App Store
  • Best for: most apps, fast iteration
Bare Workflow / CLI
  • Full Xcode + Android Studio control
  • Any native module available
  • Custom native code (Swift/Kotlin)
  • Requires iOS/Android dev environment
  • Best for: complex native integrations
Start with Expo

The vast majority of apps are well-served by Expo's managed workflow. You can always "eject" to a bare workflow later if you need custom native code. Starting bare means managing Xcode and Android Studio from day one — unnecessary overhead for most projects.

2

Project Setup

bashBootstrap with Expo + TypeScript
npx create-expo-app@latest MyApp --template blank-typescript
cd MyApp
npx expo install expo-router react-native-safe-area-context react-native-screens
npm install @tanstack/react-query zustand axios
jsonapp.json (Expo config)
{
  "expo": {
    "name": "MyApp",
    "slug": "myapp",
    "version": "1.0.0",
    "scheme": "myapp",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#0e1828"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.myorg.myapp",
      "infoPlist": {
        "NSLocationWhenInUseUsageDescription": "Used to show nearby content",
        "NSCameraUsageDescription": "Used to capture photos"
      }
    },
    "android": {
      "package": "com.myorg.myapp",
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#0e1828"
      },
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
    },
    "plugins": [
      "expo-router",
      ["expo-notifications", { "sounds": ["./assets/notification.wav"] }]
    ]
  }
}
3

Navigation

Expo Router uses a file-based routing system (similar to Next.js) built on React Navigation. Files in the app/ directory automatically become routes.

textapp/ directory structure
app/
├── _layout.tsx          # Root layout (providers, auth guard)
├── index.tsx            # "/" → Home screen
├── (tabs)/
│   ├── _layout.tsx      # Tab bar layout
│   ├── feed.tsx         # "/feed"
│   ├── search.tsx       # "/search"
│   └── profile.tsx      # "/profile"
└── post/
    └── [id].tsx         # "/post/:id" dynamic route
tsxapp/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    
       (
            
          ),
        }}
      />
       (
            
          ),
        }}
      />
    
  );
}
4

State Management

Separate server state (remote data — use TanStack Query) from client state (UI state — use Zustand or Context). This separation keeps components clean and avoids the complexity of managing cache invalidation manually.

typescriptstore/authStore.ts (Zustand)
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface AuthState {
  token:   string | null;
  userId:  string | null;
  setAuth: (token: string, userId: string) => void;
  logout:  () => void;
}

export const useAuthStore = create()(
  persist(
    (set) => ({
      token:   null,
      userId:  null,
      setAuth: (token, userId) => set({ token, userId }),
      logout:  () => set({ token: null, userId: null }),
    }),
    {
      name:    'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);
tsxhooks/useFeed.ts (TanStack Query)
import { useInfiniteQuery } from '@tanstack/react-query';
import { api } from '../services/api';

export function useFeed() {
  return useInfiniteQuery({
    queryKey: ['feed'],
    queryFn:  ({ pageParam = 1 }) =>
      api.get(`/posts?page=${pageParam}&limit=15`).then(r => r.data),
    getNextPageParam: (lastPage) =>
      lastPage.meta.page < lastPage.meta.pages
        ? lastPage.meta.page + 1
        : undefined,
    initialPageParam: 1,
    staleTime: 2 * 60 * 1000,   // 2 minutes
  });
}
5

Native UI Components

React Native's core components map directly to native UI widgets. View = UIView / android.view.View. Layout uses Flexbox with the same properties as CSS, but with different defaults (column direction, no inherited styles).

tsxPerformant FlatList with skeleton loading
import { FlatList, View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useFeed } from '../hooks/useFeed';
import { PostCard } from './PostCard';

export function FeedScreen() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useFeed();

  const posts = data?.pages.flatMap(p => p.data) ?? [];

  if (isLoading) return ;

  return (
     item.id}
      renderItem={({ item }) => }
      onEndReached={() => hasNextPage && fetchNextPage()}
      onEndReachedThreshold={0.5}
      contentContainerStyle={styles.list}
      ListFooterComponent={
        isFetchingNextPage
          ? 
          : null
      }
      // Performance optimisations
      removeClippedSubviews
      maxToRenderPerBatch={8}
      windowSize={10}
      initialNumToRender={6}
    />
  );
}

const styles = StyleSheet.create({
  loader: { flex: 1, justifyContent: 'center' },
  list:   { paddingHorizontal: 16, paddingTop: 8 },
  footer: { paddingVertical: 20 },
});
6

Native Module Integration

Expo provides JavaScript wrappers for most native APIs. For custom native functionality, use Expo Modules API to write Swift/Kotlin native modules with a clean TypeScript interface.

tsxCamera + geolocation + push notifications
import * as Camera         from 'expo-camera';
import * as Location       from 'expo-location';
import * as Notifications  from 'expo-notifications';

// Camera
async function takePicture() {
  const { status } = await Camera.requestCameraPermissionsAsync();
  if (status !== 'granted') return;
  // Camera component handles the rest
}

// Geolocation
async function getCurrentLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') throw new Error('Location denied');
  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced,
  });
  return { lat: location.coords.latitude, lng: location.coords.longitude };
}

// Push notifications
async function registerForPushNotifications(): Promise {
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') return null;

  const token = await Notifications.getExpoPushTokenAsync({
    projectId: process.env.EXPO_PUBLIC_PROJECT_ID,
  });
  return token.data;  // Send this to your server
}

// Handle foreground notifications
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge:  true,
  }),
});
7

API Integration

Use Axios with TanStack Query for most API needs. Configure a base Axios instance that reads the auth token from Zustand and handles token refresh — the same pattern as the full-stack guide.

typescriptservices/api.ts (mobile)
import axios from 'axios';
import { useAuthStore } from '../store/authStore';

export const api = axios.create({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  timeout: 10_000,
});

api.interceptors.request.use(config => {
  const token = useAuthStore.getState().token;
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

api.interceptors.response.use(
  res => res,
  async err => {
    if (err.response?.status === 401) {
      useAuthStore.getState().logout();
    }
    return Promise.reject(err);
  }
);

// Offline support: queue failed requests
import NetInfo from '@react-native-community/netinfo';

NetInfo.addEventListener(state => {
  if (state.isConnected) {
    // flush queued requests
  }
});
Mobile-Specific API Considerations
  • Always set a timeout — mobile networks are unreliable. 10 seconds is a reasonable default.
  • Use TanStack Query's networkMode: 'offlineFirst' to serve stale cache data when offline.
  • Implement exponential backoff on retry for failed network requests.
8

Testing and Publishing

Test React Native apps with Jest (unit/integration) and Detox (E2E on real simulators/devices). Use EAS Build to produce App Store and Google Play binaries without managing Xcode or Android Studio locally.

tsxComponent test with @testing-library/react-native
import { render, screen, fireEvent } from '@testing-library/react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { LoginScreen } from '../screens/LoginScreen';

const wrapper = ({ children }: any) => (
  
    {children}
  
);

describe('LoginScreen', () => {
  it('shows validation error for empty email', async () => {
    render(, { wrapper });

    fireEvent.press(screen.getByRole('button', { name: /sign in/i }));

    expect(await screen.findByText(/email is required/i)).toBeTruthy();
  });

  it('calls login with correct credentials', async () => {
    const mockLogin = jest.fn();
    render(, { wrapper });

    fireEvent.changeText(screen.getByPlaceholderText(/email/i), 'test@example.com');
    fireEvent.changeText(screen.getByPlaceholderText(/password/i), 'password123');
    fireEvent.press(screen.getByRole('button', { name: /sign in/i }));

    await screen.findByText(/welcome/i);
    expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123');
  });
});
bashEAS Build + Submit
# Install EAS CLI
npm install -g eas-cli && eas login

# Configure builds
eas build:configure

# Build for both platforms (production)
eas build --platform all --profile production

# Submit to App Store Connect and Google Play
eas submit --platform ios    --profile production
eas submit --platform android --profile production

# OTA update (no app store review needed for JS changes)
eas update --branch production --message "Fix login bug"