Back to Blog
Development Tips17 min read

React Native Tutorial 2026: Build Your First Android & iOS App

Complete React Native tutorial for beginners. Step-by-step guide to build your first mobile app with Expo, navigation, API integration, and state management. Includes Android setup and production tips.

Hevcode Team
January 5, 2026

React Native powers apps used by billions — Facebook, Instagram, Discord, Shopify, and Pinterest. This tutorial takes you from zero to a production-ready app with navigation, API integration, and state management. Works for both Android and iOS.

Why React Native in 2026?

Framework Performance Development Speed Code Sharing Market Share
React Native Near-native Fast 90%+ 38%
Flutter Near-native Fast 95%+ 42%
Native (Swift/Kotlin) Best Slow 0% 20%

React Native Advantages

  • Huge ecosystem: 1M+ npm packages, largest community
  • JavaScript: Most developers already know it
  • Hot reloading: See changes in milliseconds
  • Web developer friendly: React skills transfer directly
  • Expo: Build without Xcode/Android Studio setup
  • OTA updates: Push updates without app store review

Prerequisites

Before starting, you should know:

  • Required: Basic JavaScript (variables, functions, arrays, objects)
  • Helpful: React basics (components, props, state)
  • Not required: Any mobile development experience

Setting Up Your Development Environment

Option 1: Expo (Recommended for Beginners)

Expo lets you build React Native apps without installing Xcode or Android Studio.

# Step 1: Install Node.js 18+ from nodejs.org

# Step 2: Create your project
npx create-expo-app@latest MyApp
cd MyApp

# Step 3: Start development server
npx expo start

Run on your device:

  1. Install "Expo Go" from App Store or Play Store
  2. Scan the QR code shown in terminal
  3. Your app loads instantly on your phone

Option 2: React Native CLI (Production Apps)

For full native control, use the bare React Native CLI.

Android Setup (Windows, Mac, Linux)

# Step 1: Install Android Studio
# Download from developer.android.com/studio

# Step 2: Install Android SDK (via Android Studio)
# - Android SDK Platform 34
# - Android SDK Build-Tools 34
# - Android Emulator
# - Android SDK Platform-Tools

# Step 3: Set environment variables
# Add to ~/.zshrc or ~/.bashrc:
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools

# Step 4: Create project
npx react-native@latest init MyApp
cd MyApp

# Step 5: Start Metro bundler
npx react-native start

# Step 6: Run on Android (new terminal)
npx react-native run-android

iOS Setup (Mac Only)

# Step 1: Install Xcode from App Store

# Step 2: Install Xcode Command Line Tools
xcode-select --install

# Step 3: Install CocoaPods
sudo gem install cocoapods

# Step 4: Create project
npx react-native@latest init MyApp
cd MyApp

# Step 5: Install iOS dependencies
cd ios && pod install && cd ..

# Step 6: Run on iOS Simulator
npx react-native run-ios

Project Structure

MyApp/
├── App.tsx                 # Main entry point
├── package.json           # Dependencies
├── tsconfig.json          # TypeScript config
├── babel.config.js        # Babel config
├── metro.config.js        # Metro bundler config
├── src/
│   ├── components/        # Reusable components
│   ├── screens/           # Screen components
│   ├── navigation/        # Navigation setup
│   ├── services/          # API calls
│   ├── hooks/             # Custom hooks
│   ├── context/           # React Context
│   └── utils/             # Helper functions
├── assets/                # Images, fonts
├── android/               # Native Android code
└── ios/                   # Native iOS code

Building Your First App: Task Manager

Let's build a complete task manager with:

  • Add/edit/delete tasks
  • Navigation between screens
  • API integration
  • Persistent storage
  • Pull-to-refresh

Step 1: Basic App with TypeScript

Replace App.tsx:

import React, { useState } from 'react';
import {
  SafeAreaView,
  StyleSheet,
  Text,
  View,
  TextInput,
  TouchableOpacity,
  FlatList,
  StatusBar,
} from 'react-native';

interface Task {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

export default function App() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [inputText, setInputText] = useState('');

  const addTask = () => {
    if (!inputText.trim()) return;

    const newTask: Task = {
      id: Date.now().toString(),
      title: inputText.trim(),
      completed: false,
      createdAt: new Date(),
    };

    setTasks(prev => [newTask, ...prev]);
    setInputText('');
  };

  const toggleTask = (id: string) => {
    setTasks(prev =>
      prev.map(task =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  const deleteTask = (id: string) => {
    setTasks(prev => prev.filter(task => task.id !== id));
  };

  const completedCount = tasks.filter(t => t.completed).length;

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />

      <View style={styles.header}>
        <Text style={styles.title}>My Tasks</Text>
        <Text style={styles.subtitle}>
          {completedCount}/{tasks.length} completed
        </Text>
      </View>

      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          placeholderTextColor="#999"
          value={inputText}
          onChangeText={setInputText}
          onSubmitEditing={addTask}
          returnKeyType="done"
        />
        <TouchableOpacity
          style={[styles.addButton, !inputText.trim() && styles.addButtonDisabled]}
          onPress={addTask}
          disabled={!inputText.trim()}
        >
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>

      <FlatList
        data={tasks}
        keyExtractor={item => item.id}
        renderItem={({ item }) => (
          <View style={[styles.taskItem, item.completed && styles.taskCompleted]}>
            <TouchableOpacity
              style={styles.taskContent}
              onPress={() => toggleTask(item.id)}
            >
              <View style={[styles.checkbox, item.completed && styles.checkboxChecked]}>
                {item.completed && <Text style={styles.checkmark}>✓</Text>}
              </View>
              <Text style={[styles.taskText, item.completed && styles.taskTextCompleted]}>
                {item.title}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.deleteButton}
              onPress={() => deleteTask(item.id)}
            >
              <Text style={styles.deleteButtonText}>×</Text>
            </TouchableOpacity>
          </View>
        )}
        ListEmptyComponent={
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyText}>No tasks yet</Text>
            <Text style={styles.emptySubtext}>Add your first task above</Text>
          </View>
        }
        contentContainerStyle={tasks.length === 0 && styles.emptyList}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  header: {
    padding: 20,
    paddingTop: 10,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#1a1a2e',
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  inputContainer: {
    flexDirection: 'row',
    paddingHorizontal: 20,
    marginBottom: 20,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  addButton: {
    width: 52,
    height: 52,
    backgroundColor: '#4361ee',
    borderRadius: 12,
    marginLeft: 12,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonDisabled: {
    backgroundColor: '#a8b5db',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 28,
    fontWeight: '300',
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    marginHorizontal: 20,
    marginBottom: 12,
    padding: 16,
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  taskCompleted: {
    backgroundColor: '#f0f0f0',
  },
  taskContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  checkbox: {
    width: 24,
    height: 24,
    borderRadius: 6,
    borderWidth: 2,
    borderColor: '#4361ee',
    marginRight: 12,
    justifyContent: 'center',
    alignItems: 'center',
  },
  checkboxChecked: {
    backgroundColor: '#4361ee',
  },
  checkmark: {
    color: '#fff',
    fontSize: 14,
    fontWeight: 'bold',
  },
  taskText: {
    fontSize: 16,
    color: '#1a1a2e',
    flex: 1,
  },
  taskTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#999',
  },
  deleteButton: {
    padding: 8,
  },
  deleteButtonText: {
    color: '#e63946',
    fontSize: 24,
    fontWeight: '300',
  },
  emptyContainer: {
    alignItems: 'center',
    paddingTop: 60,
  },
  emptyText: {
    fontSize: 18,
    color: '#666',
    marginBottom: 8,
  },
  emptySubtext: {
    fontSize: 14,
    color: '#999',
  },
  emptyList: {
    flex: 1,
  },
});

Step 2: Add Navigation

Install React Navigation:

# For Expo
npx expo install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

# For bare React Native
npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context
cd ios && pod install && cd ..

Create navigation structure:

// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from '../screens/HomeScreen';
import TaskDetailScreen from '../screens/TaskDetailScreen';
import AddTaskScreen from '../screens/AddTaskScreen';

export type RootStackParamList = {
  Home: undefined;
  TaskDetail: { taskId: string };
  AddTask: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerStyle: { backgroundColor: '#4361ee' },
          headerTintColor: '#fff',
          headerTitleStyle: { fontWeight: 'bold' },
        }}
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'My Tasks' }}
        />
        <Stack.Screen
          name="TaskDetail"
          component={TaskDetailScreen}
          options={{ title: 'Task Details' }}
        />
        <Stack.Screen
          name="AddTask"
          component={AddTaskScreen}
          options={{
            title: 'Add Task',
            presentation: 'modal',
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
// src/screens/HomeScreen.tsx
import React from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/AppNavigator';

type HomeScreenProps = {
  navigation: NativeStackNavigationProp<RootStackParamList, 'Home'>;
};

export default function HomeScreen({ navigation }: HomeScreenProps) {
  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={styles.addButton}
        onPress={() => navigation.navigate('AddTask')}
      >
        <Text style={styles.addButtonText}>+ Add Task</Text>
      </TouchableOpacity>

      {/* Task list here */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  addButton: {
    backgroundColor: '#4361ee',
    margin: 20,
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Step 3: API Integration

Create an API service for fetching data:

// src/services/api.ts
const API_BASE_URL = 'https://jsonplaceholder.typicode.com';

export interface Todo {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}

class ApiService {
  private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${endpoint}`, {
      headers: {
        'Content-Type': 'application/json',
      },
      ...options,
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }

    return response.json();
  }

  async getTodos(): Promise<Todo[]> {
    return this.request<Todo[]>('/todos?_limit=20');
  }

  async getTodo(id: number): Promise<Todo> {
    return this.request<Todo>(`/todos/${id}`);
  }

  async createTodo(todo: Omit<Todo, 'id'>): Promise<Todo> {
    return this.request<Todo>('/todos', {
      method: 'POST',
      body: JSON.stringify(todo),
    });
  }

  async updateTodo(id: number, updates: Partial<Todo>): Promise<Todo> {
    return this.request<Todo>(`/todos/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(updates),
    });
  }

  async deleteTodo(id: number): Promise<void> {
    await this.request(`/todos/${id}`, { method: 'DELETE' });
  }
}

export const api = new ApiService();

Step 4: Custom Hook for Data Fetching

// src/hooks/useTodos.ts
import { useState, useEffect, useCallback } from 'react';
import { api, Todo } from '../services/api';

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [refreshing, setRefreshing] = useState(false);

  const fetchTodos = useCallback(async () => {
    try {
      setError(null);
      const data = await api.getTodos();
      setTodos(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to fetch todos');
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, []);

  useEffect(() => {
    fetchTodos();
  }, [fetchTodos]);

  const refresh = useCallback(() => {
    setRefreshing(true);
    fetchTodos();
  }, [fetchTodos]);

  const addTodo = useCallback(async (title: string) => {
    try {
      const newTodo = await api.createTodo({
        userId: 1,
        title,
        completed: false,
      });
      setTodos(prev => [newTodo, ...prev]);
      return newTodo;
    } catch (err) {
      throw err;
    }
  }, []);

  const toggleTodo = useCallback(async (id: number) => {
    const todo = todos.find(t => t.id === id);
    if (!todo) return;

    // Optimistic update
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, completed: !t.completed } : t))
    );

    try {
      await api.updateTodo(id, { completed: !todo.completed });
    } catch (err) {
      // Rollback on error
      setTodos(prev =>
        prev.map(t => (t.id === id ? { ...t, completed: todo.completed } : t))
      );
      throw err;
    }
  }, [todos]);

  const deleteTodo = useCallback(async (id: number) => {
    const todoIndex = todos.findIndex(t => t.id === id);
    const deletedTodo = todos[todoIndex];

    // Optimistic update
    setTodos(prev => prev.filter(t => t.id !== id));

    try {
      await api.deleteTodo(id);
    } catch (err) {
      // Rollback on error
      setTodos(prev => {
        const newTodos = [...prev];
        newTodos.splice(todoIndex, 0, deletedTodo);
        return newTodos;
      });
      throw err;
    }
  }, [todos]);

  return {
    todos,
    loading,
    error,
    refreshing,
    refresh,
    addTodo,
    toggleTodo,
    deleteTodo,
  };
}

Step 5: State Management with Context

// src/context/TaskContext.tsx
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Task {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate?: Date;
}

interface TaskState {
  tasks: Task[];
  filter: 'all' | 'active' | 'completed';
}

type TaskAction =
  | { type: 'ADD_TASK'; payload: Task }
  | { type: 'UPDATE_TASK'; payload: { id: string; updates: Partial<Task> } }
  | { type: 'DELETE_TASK'; payload: string }
  | { type: 'TOGGLE_TASK'; payload: string }
  | { type: 'SET_FILTER'; payload: TaskState['filter'] }
  | { type: 'LOAD_TASKS'; payload: Task[] };

const initialState: TaskState = {
  tasks: [],
  filter: 'all',
};

function taskReducer(state: TaskState, action: TaskAction): TaskState {
  switch (action.type) {
    case 'ADD_TASK':
      return { ...state, tasks: [action.payload, ...state.tasks] };

    case 'UPDATE_TASK':
      return {
        ...state,
        tasks: state.tasks.map(task =>
          task.id === action.payload.id
            ? { ...task, ...action.payload.updates }
            : task
        ),
      };

    case 'DELETE_TASK':
      return {
        ...state,
        tasks: state.tasks.filter(task => task.id !== action.payload),
      };

    case 'TOGGLE_TASK':
      return {
        ...state,
        tasks: state.tasks.map(task =>
          task.id === action.payload
            ? { ...task, completed: !task.completed }
            : task
        ),
      };

    case 'SET_FILTER':
      return { ...state, filter: action.payload };

    case 'LOAD_TASKS':
      return { ...state, tasks: action.payload };

    default:
      return state;
  }
}

const TaskContext = createContext<{
  state: TaskState;
  dispatch: React.Dispatch<TaskAction>;
} | null>(null);

export function TaskProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(taskReducer, initialState);

  return (
    <TaskContext.Provider value={{ state, dispatch }}>
      {children}
    </TaskContext.Provider>
  );
}

export function useTaskContext() {
  const context = useContext(TaskContext);
  if (!context) {
    throw new Error('useTaskContext must be used within TaskProvider');
  }
  return context;
}

// Selector hooks for filtered data
export function useFilteredTasks() {
  const { state } = useTaskContext();

  switch (state.filter) {
    case 'active':
      return state.tasks.filter(t => !t.completed);
    case 'completed':
      return state.tasks.filter(t => t.completed);
    default:
      return state.tasks;
  }
}

Step 6: Persistent Storage

// src/utils/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE_KEYS = {
  TASKS: '@tasks',
  USER_PREFERENCES: '@user_preferences',
};

export const storage = {
  async saveTasks(tasks: any[]) {
    try {
      await AsyncStorage.setItem(STORAGE_KEYS.TASKS, JSON.stringify(tasks));
    } catch (error) {
      console.error('Failed to save tasks:', error);
    }
  },

  async loadTasks() {
    try {
      const data = await AsyncStorage.getItem(STORAGE_KEYS.TASKS);
      return data ? JSON.parse(data) : [];
    } catch (error) {
      console.error('Failed to load tasks:', error);
      return [];
    }
  },

  async savePreferences(prefs: object) {
    try {
      await AsyncStorage.setItem(STORAGE_KEYS.USER_PREFERENCES, JSON.stringify(prefs));
    } catch (error) {
      console.error('Failed to save preferences:', error);
    }
  },

  async loadPreferences() {
    try {
      const data = await AsyncStorage.getItem(STORAGE_KEYS.USER_PREFERENCES);
      return data ? JSON.parse(data) : {};
    } catch (error) {
      console.error('Failed to load preferences:', error);
      return {};
    }
  },

  async clear() {
    try {
      await AsyncStorage.clear();
    } catch (error) {
      console.error('Failed to clear storage:', error);
    }
  },
};

Install AsyncStorage:

# Expo
npx expo install @react-native-async-storage/async-storage

# Bare React Native
npm install @react-native-async-storage/async-storage
cd ios && pod install && cd ..

Essential React Native Concepts

Core Components

Component Web Equivalent Usage
View <div> Container for layout
Text <p>, <span> All text must be wrapped
Image <img> Display images
ScrollView <div> with overflow Scrollable container
FlatList Virtual list Efficient long lists
TextInput <input> Text input field
TouchableOpacity <button> Clickable with opacity feedback
Pressable <button> More customizable clicks

Styling with StyleSheet

import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,                    // Flexbox (default)
    padding: 20,                // No 'px' unit
    backgroundColor: '#fff',
    justifyContent: 'center',   // Main axis
    alignItems: 'center',       // Cross axis
  },
  text: {
    fontSize: 16,
    fontWeight: '600',          // String, not number
    color: '#333',
    textAlign: 'center',
  },
  shadow: {
    // iOS shadow
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    // Android shadow
    elevation: 3,
  },
});

Platform-Specific Code

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    paddingTop: Platform.OS === 'ios' ? 44 : 0,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
      },
      android: {
        elevation: 4,
      },
    }),
  },
});

// Platform-specific files
// Button.ios.tsx - iOS version
// Button.android.tsx - Android version
// Import as: import Button from './Button';

Production Tips

Performance Optimization

import React, { memo, useCallback, useMemo } from 'react';
import { FlatList } from 'react-native';

// 1. Memoize components
const TaskItem = memo(({ task, onToggle, onDelete }) => {
  // Only re-renders when props change
  return (
    <View>
      <Text>{task.title}</Text>
    </View>
  );
});

// 2. Memoize callbacks
function TaskList({ tasks }) {
  const handleToggle = useCallback((id) => {
    // Toggle logic
  }, []);

  // 3. Memoize expensive calculations
  const completedCount = useMemo(
    () => tasks.filter(t => t.completed).length,
    [tasks]
  );

  // 4. Optimize FlatList
  return (
    <FlatList
      data={tasks}
      renderItem={({ item }) => (
        <TaskItem task={item} onToggle={handleToggle} />
      )}
      keyExtractor={item => item.id}
      removeClippedSubviews={true}           // Unmount off-screen items
      maxToRenderPerBatch={10}               // Render 10 items per batch
      windowSize={5}                          // Render 5 screens worth
      initialNumToRender={10}                // Initial render count
      getItemLayout={(data, index) => ({     // Skip measurement
        length: 80,
        offset: 80 * index,
        index,
      })}
    />
  );
}

Error Boundaries

// src/components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error caught:', error, errorInfo);
    // Send to error tracking service (Sentry, Bugsnag, etc.)
  }

  handleRetry = () => {
    this.setState({ hasError: false, error: undefined });
  };

  render() {
    if (this.state.hasError) {
      return (
        <View style={styles.container}>
          <Text style={styles.title}>Something went wrong</Text>
          <Text style={styles.message}>{this.state.error?.message}</Text>
          <TouchableOpacity style={styles.button} onPress={this.handleRetry}>
            <Text style={styles.buttonText}>Try Again</Text>
          </TouchableOpacity>
        </View>
      );
    }

    return this.props.children;
  }
}

Building for Production

# Android APK (debug)
cd android && ./gradlew assembleDebug

# Android APK (release)
cd android && ./gradlew assembleRelease

# Android AAB (for Play Store)
cd android && ./gradlew bundleRelease

# iOS (use Xcode)
# Open ios/MyApp.xcworkspace
# Select "Any iOS Device" as target
# Product > Archive

For secure app builds and release management, see our Mobile App Security Best Practices guide.

Common Mistakes to Avoid

Mistake Problem Solution
Text outside <Text> Crash Always wrap text in <Text>
Using ScrollView for long lists Poor performance Use FlatList instead
Inline styles Re-renders Use StyleSheet.create
Missing key prop Warnings, bugs Add unique key to list items
Not handling keyboard Input hidden Use KeyboardAvoidingView
Ignoring Android Bugs on Android Test on both platforms

Frequently Asked Questions

Is React Native good for beginners?

Yes. If you know JavaScript and basic React, you can build mobile apps quickly. Expo makes setup effortless — no Xcode or Android Studio needed initially. The learning curve is gentler than native iOS (Swift) or Android (Kotlin) development.

React Native vs Flutter: Which should I learn?

Learn React Native if you already know JavaScript/React or plan to also do web development. Learn Flutter if you're starting fresh and want slightly better performance. Both are excellent choices with strong job markets. React Native has a larger community; Flutter is growing faster.

Can I build production apps with Expo?

Yes. Expo now supports custom native code via "development builds." Many production apps use Expo, including parts of Shopify, Discord, and Coinbase. Start with Expo for simplicity; eject to bare workflow only if you need specific native modules not supported by Expo.

How long does it take to learn React Native?

With JavaScript knowledge: 2-4 weeks to build basic apps, 2-3 months for production-quality apps. Without JavaScript: add 1-2 months for JavaScript fundamentals. The key is building real projects, not just following tutorials.

What's the difference between React Native CLI and Expo?

Expo: Managed workflow, no native code access initially, easier setup, OTA updates, limited native modules. React Native CLI: Full native access, requires Xcode/Android Studio, more setup, unlimited customization. Start with Expo; migrate if needed.

Ready to Build Your App?

You now have the foundation to build real mobile apps. The next step is building something you care about — that's how you truly learn.

If you need help bringing your app idea to production, we build React Native apps for businesses — from MVPs to enterprise solutions.

Get in touch:


Related Articles

Tags:React NativeTutorialAndroidiOSMobile DevelopmentExpoJavaScript

Need help with your project?

We've helped 534+ clients build successful apps. Let's discuss yours.

Ready to Build Your App?

534+ projects delivered • 4.9★ rating • 6+ years experience

Let's discuss your project — no obligations, just a straightforward conversation.