Toolmingo
Tutorials21 min read

React Native Tutorial for Beginners (2025 Complete Guide)

Learn React Native from scratch in 2025 — set up your environment, build your first app, master components, navigation, state management, APIs, and deploy to iOS and Android.

React Native lets you build real iOS and Android apps using JavaScript — no Swift, no Kotlin required. One codebase, two platforms, production-ready apps. This tutorial takes you from zero to a working app step by step.

What you'll learn

  • Setting up a React Native development environment
  • Core components and styling
  • Navigation between screens
  • State management with hooks and Zustand
  • Fetching data from APIs
  • Storing data locally
  • Device features (camera, location, notifications)
  • Publishing to the App Store and Play Store

Prerequisites

You need basic JavaScript knowledge (variables, functions, arrays, objects). React experience helps but isn't required — this tutorial covers what you need.


1. Environment setup

Option A — Expo (recommended for beginners)

Expo is the fastest way to start. No Xcode or Android Studio required.

# Install Expo CLI
npm install -g expo-cli

# Create a new project
npx create-expo-app MyFirstApp
cd MyFirstApp

# Start the development server
npx expo start

Scan the QR code with the Expo Go app on your phone. Your app runs instantly.

Option B — React Native CLI (full control)

For full native access, use the React Native CLI.

macOS (for iOS development):

# Install Xcode from the App Store, then:
xcode-select --install

# Install CocoaPods
sudo gem install cocoapods

# Install Android Studio (for Android)
# Set ANDROID_HOME in ~/.zshrc:
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools

Windows (Android only):

# Install Android Studio
# Set ANDROID_HOME in System Environment Variables:
# ANDROID_HOME = C:\Users\<YourUser>\AppData\Local\Android\Sdk

Create and run the project:

npx react-native@latest init MyFirstApp
cd MyFirstApp

# iOS
npx react-native run-ios

# Android
npx react-native run-android

Expo vs React Native CLI

Feature Expo React Native CLI
Setup time 5 minutes 1–2 hours
Native modules Limited (but growing) Full access
OTA updates Yes (expo-updates) Manual setup
Custom native code Expo dev client Yes
Build service EAS Build Self-managed
Best for Beginners, MVPs Full control, custom native

2. Project structure

A new Expo project looks like this:

MyFirstApp/
├── app/                  # Screens (Expo Router)
│   ├── (tabs)/
│   │   ├── index.tsx     # Home tab
│   │   └── explore.tsx   # Explore tab
│   └── _layout.tsx       # Root layout
├── components/           # Reusable components
├── assets/               # Images, fonts
├── constants/            # Colors, config
└── package.json

Or for a basic Expo project (no router):

MyFirstApp/
├── App.tsx               # Entry point
├── components/
├── screens/
├── assets/
└── package.json

3. Core components

React Native has its own components — not HTML elements.

Web (HTML) React Native Purpose
<div> <View> Container/layout
<p>, <span> <Text> Display text
<img> <Image> Display images
<input> <TextInput> Text input
<button> <TouchableOpacity> / <Pressable> Tappable element
<ul>, <ol> <FlatList> Scrollable lists
<div> (scrollable) <ScrollView> Scroll container

Your first component

// App.tsx
import { View, Text, StyleSheet } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Hello, React Native!</Text>
      <Text style={styles.subtitle}>Welcome to mobile development</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1a1a1a',
  },
  subtitle: {
    fontSize: 16,
    color: '#666',
    marginTop: 8,
  },
});

4. Styling in React Native

React Native uses JavaScript objects for styles — no CSS files. Properties use camelCase.

Flexbox layout

React Native uses Flexbox for layout (default direction is column, unlike web where it's row).

import { View, Text, StyleSheet } from 'react-native';

export default function Layout() {
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerText}>Header</Text>
      </View>

      <View style={styles.row}>
        <View style={styles.box}>
          <Text>Box 1</Text>
        </View>
        <View style={styles.box}>
          <Text>Box 2</Text>
        </View>
        <View style={styles.box}>
          <Text>Box 3</Text>
        </View>
      </View>

      <View style={styles.footer}>
        <Text>Footer</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,             // fill available space
    padding: 16,
  },
  header: {
    backgroundColor: '#6200ea',
    padding: 20,
    borderRadius: 8,
  },
  headerText: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
  row: {
    flexDirection: 'row',          // horizontal layout
    justifyContent: 'space-between', // space between items
    marginTop: 16,
  },
  box: {
    flex: 1,            // equal width
    backgroundColor: '#e8e8e8',
    padding: 16,
    margin: 4,
    borderRadius: 8,
    alignItems: 'center',
  },
  footer: {
    marginTop: 'auto', // push to bottom
    padding: 16,
    alignItems: 'center',
  },
});

Common style properties

const styles = StyleSheet.create({
  box: {
    // Size
    width: 100,
    height: 100,
    // or: width: '50%'

    // Spacing
    margin: 8,
    marginHorizontal: 16,
    padding: 12,
    paddingVertical: 8,

    // Background and border
    backgroundColor: '#fff',
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#ddd',

    // Shadows (iOS)
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,

    // Elevation (Android shadow)
    elevation: 3,
  },
  text: {
    fontSize: 16,
    fontWeight: '600',    // '400' | '600' | '700' | 'bold'
    color: '#1a1a1a',
    textAlign: 'center',
    letterSpacing: 0.5,
    lineHeight: 24,
  },
});

5. State and interactivity

useState hook

import { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <View style={styles.container}>
      <Text style={styles.count}>{count}</Text>

      <View style={styles.buttons}>
        <TouchableOpacity
          style={[styles.button, styles.decrement]}
          onPress={() => setCount(c => c - 1)}
        >
          <Text style={styles.buttonText}>−</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.button, styles.increment]}
          onPress={() => setCount(c => c + 1)}
        >
          <Text style={styles.buttonText}>+</Text>
        </TouchableOpacity>
      </View>

      <TouchableOpacity onPress={() => setCount(0)}>
        <Text style={styles.reset}>Reset</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  count: { fontSize: 72, fontWeight: 'bold', marginBottom: 32 },
  buttons: { flexDirection: 'row', gap: 16 },
  button: {
    width: 64, height: 64, borderRadius: 32,
    alignItems: 'center', justifyContent: 'center',
  },
  decrement: { backgroundColor: '#ff5252' },
  increment: { backgroundColor: '#00c853' },
  buttonText: { fontSize: 32, color: 'white', fontWeight: 'bold' },
  reset: { marginTop: 24, color: '#666', fontSize: 16 },
});

TextInput — handling user input

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

export default function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  function handleLogin() {
    if (!email || !password) {
      setError('Both fields are required.');
      return;
    }
    setError('');
    console.log('Logging in:', email);
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Login</Text>

      {error ? <Text style={styles.error}>{error}</Text> : null}

      <TextInput
        style={styles.input}
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        keyboardType="email-address"
        autoCapitalize="none"
        autoCorrect={false}
      />

      <TextInput
        style={styles.input}
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />

      <TouchableOpacity style={styles.button} onPress={handleLogin}>
        <Text style={styles.buttonText}>Login</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, justifyContent: 'center' },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },
  error: { color: 'red', marginBottom: 12 },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
    fontSize: 16,
    backgroundColor: 'white',
  },
  button: {
    backgroundColor: '#6200ea',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

6. Lists

FlatList — efficient scrollable lists

import { FlatList, View, Text, StyleSheet, TouchableOpacity } from 'react-native';

interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}

const USERS: User[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
  { id: '3', name: 'Carol Davis', email: 'carol@example.com', role: 'Manager' },
  // ...more users
];

function UserCard({ user }: { user: User }) {
  return (
    <TouchableOpacity style={styles.card}>
      <View style={styles.avatar}>
        <Text style={styles.avatarText}>{user.name[0]}</Text>
      </View>
      <View style={styles.info}>
        <Text style={styles.name}>{user.name}</Text>
        <Text style={styles.email}>{user.email}</Text>
        <Text style={styles.role}>{user.role}</Text>
      </View>
    </TouchableOpacity>
  );
}

export default function UserList() {
  return (
    <FlatList
      data={USERS}
      keyExtractor={item => item.id}
      renderItem={({ item }) => <UserCard user={item} />}
      contentContainerStyle={styles.list}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
    />
  );
}

const styles = StyleSheet.create({
  list: { padding: 16 },
  card: {
    flexDirection: 'row',
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 16,
  },
  avatar: {
    width: 48, height: 48, borderRadius: 24,
    backgroundColor: '#6200ea',
    alignItems: 'center', justifyContent: 'center',
    marginRight: 12,
  },
  avatarText: { color: 'white', fontSize: 20, fontWeight: 'bold' },
  info: { flex: 1 },
  name: { fontSize: 16, fontWeight: '600', color: '#1a1a1a' },
  email: { fontSize: 14, color: '#666', marginTop: 2 },
  role: { fontSize: 12, color: '#6200ea', marginTop: 4 },
  separator: { height: 8 },
});

FlatList props reference

Prop Purpose Example
data Array to render data={users}
keyExtractor Unique key per item item => item.id
renderItem Render each item ({ item }) => <Card item={item} />
onEndReached Load more on scroll () => fetchMore()
onEndReachedThreshold When to trigger 0.5 (50% from bottom)
refreshing Pull-to-refresh state {isRefreshing}
onRefresh Pull-to-refresh handler () => refetch()
ListHeaderComponent Header element <SearchBar />
ListEmptyComponent Empty state <EmptyState />
numColumns Grid layout 2
horizontal Horizontal scroll true

7. Navigation

The standard navigation library for React Native is React Navigation.

npm install @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context

Stack navigator

// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';

export type RootStackParamList = {
  Home: undefined;
  Detail: { id: string; title: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerStyle: { backgroundColor: '#6200ea' },
          headerTintColor: 'white',
          headerTitleStyle: { fontWeight: 'bold' },
        }}
      >
        <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'My App' }} />
        <Stack.Screen
          name="Detail"
          component={DetailScreen}
          options={({ route }) => ({ title: route.params.title })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
// screens/HomeScreen.tsx
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../App';

type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;

const ITEMS = [
  { id: '1', title: 'Introduction to React Native' },
  { id: '2', title: 'Components and Styling' },
  { id: '3', title: 'Navigation Deep Dive' },
];

export default function HomeScreen({ navigation }: Props) {
  return (
    <FlatList
      data={ITEMS}
      keyExtractor={item => item.id}
      contentContainerStyle={styles.list}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={styles.item}
          onPress={() => navigation.navigate('Detail', { id: item.id, title: item.title })}
        >
          <Text style={styles.itemTitle}>{item.title}</Text>
          <Text style={styles.arrow}>›</Text>
        </TouchableOpacity>
      )}
    />
  );
}

const styles = StyleSheet.create({
  list: { padding: 16 },
  item: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: 'white',
    padding: 16,
    borderRadius: 8,
    marginBottom: 8,
  },
  itemTitle: { fontSize: 16, color: '#1a1a1a' },
  arrow: { fontSize: 22, color: '#6200ea' },
});
// screens/DetailScreen.tsx
import { View, Text, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../App';

type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>;

export default function DetailScreen({ route }: Props) {
  const { id, title } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.id}>Article #{id}</Text>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.body}>
        This is the detail screen for "{title}". In a real app, you'd fetch
        content from an API using the id.
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  id: { color: '#6200ea', fontSize: 14, fontWeight: '600', marginBottom: 8 },
  title: { fontSize: 24, fontWeight: 'bold', color: '#1a1a1a', marginBottom: 16 },
  body: { fontSize: 16, color: '#444', lineHeight: 24 },
});

Tab navigator

npm install @react-navigation/bottom-tabs
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';

const Tab = createBottomTabNavigator();

function AppTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size }) => {
          const icons: Record<string, string> = {
            Home: 'home',
            Search: 'search',
            Profile: 'person',
          };
          return <Ionicons name={icons[route.name] as any} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#6200ea',
        tabBarInactiveTintColor: '#999',
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

Navigation types overview

Navigator Use case Package
Stack Push/pop screens (drill-down) @react-navigation/native-stack
Bottom tabs Main sections of an app @react-navigation/bottom-tabs
Drawer Side menu navigation @react-navigation/drawer
Top tabs Swipeable tab sections @react-navigation/material-top-tabs

8. Fetching data from APIs

import { useEffect, useState } from 'react';
import {
  View, Text, FlatList, ActivityIndicator,
  TouchableOpacity, StyleSheet
} from 'react-native';

interface Post {
  id: number;
  title: string;
  body: string;
}

export default function PostsScreen() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  async function fetchPosts() {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=20');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      setPosts(data);
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Failed to fetch posts');
    } finally {
      setLoading(false);
    }
  }

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

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#6200ea" />
        <Text style={styles.loadingText}>Loading posts...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>Error: {error}</Text>
        <TouchableOpacity style={styles.retryButton} onPress={fetchPosts}>
          <Text style={styles.retryText}>Retry</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={item => item.id.toString()}
      contentContainerStyle={styles.list}
      renderItem={({ item }) => (
        <View style={styles.card}>
          <Text style={styles.cardTitle}>{item.title}</Text>
          <Text style={styles.cardBody} numberOfLines={2}>
            {item.body}
          </Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
  loadingText: { marginTop: 12, color: '#666' },
  errorText: { color: '#c62828', fontSize: 16, textAlign: 'center', marginBottom: 16 },
  retryButton: { backgroundColor: '#6200ea', padding: 12, borderRadius: 8 },
  retryText: { color: 'white', fontWeight: '600' },
  list: { padding: 16 },
  card: { backgroundColor: 'white', borderRadius: 8, padding: 16, marginBottom: 8 },
  cardTitle: { fontSize: 16, fontWeight: '600', color: '#1a1a1a', marginBottom: 8, textTransform: 'capitalize' },
  cardBody: { fontSize: 14, color: '#666', lineHeight: 20 },
});

9. Local storage

AsyncStorage — simple key-value storage

npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';

// Store a value
await AsyncStorage.setItem('userId', '42');
await AsyncStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));

// Read a value
const userId = await AsyncStorage.getItem('userId');
const rawUser = await AsyncStorage.getItem('user');
const user = rawUser ? JSON.parse(rawUser) : null;

// Remove
await AsyncStorage.removeItem('userId');

// Clear everything
await AsyncStorage.clear();

In a hook:

import { useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

export function useTheme() {
  const [isDark, setIsDark] = useState(false);

  useEffect(() => {
    AsyncStorage.getItem('theme').then(value => {
      if (value === 'dark') setIsDark(true);
    });
  }, []);

  async function toggleTheme() {
    const next = !isDark;
    setIsDark(next);
    await AsyncStorage.setItem('theme', next ? 'dark' : 'light');
  }

  return { isDark, toggleTheme };
}

Storage options comparison

Solution Type Use case
AsyncStorage Key-value Settings, tokens, small data
SQLite (expo-sqlite) Relational DB Structured local data
MMKV Fast key-value High-frequency reads/writes
Zustand + AsyncStorage State + persistence App-wide state
Secure Store (expo-secure-store) Encrypted Passwords, secrets, tokens

10. State management

useState + useContext (small apps)

// contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface AuthContextType {
  user: { id: string; name: string } | null;
  login: (name: string) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<{ id: string; name: string } | null>(null);

  function login(name: string) {
    setUser({ id: Date.now().toString(), name });
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

Zustand (recommended for most apps)

npm install zustand
// stores/cartStore.ts
import { create } from 'zustand';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  total: () => number;
}

export const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (item) => set(state => {
    const existing = state.items.find(i => i.id === item.id);
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        ),
      };
    }
    return { items: [...state.items, { ...item, quantity: 1 }] };
  }),

  removeItem: (id) => set(state => ({
    items: state.items.filter(i => i.id !== id),
  })),

  updateQuantity: (id, quantity) => set(state => ({
    items: quantity <= 0
      ? state.items.filter(i => i.id !== id)
      : state.items.map(i => i.id === id ? { ...i, quantity } : i),
  })),

  clearCart: () => set({ items: [] }),

  total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
// Usage in a component
import { useCartStore } from '../stores/cartStore';

export default function CartScreen() {
  const { items, removeItem, updateQuantity, total } = useCartStore();

  return (
    <View style={styles.container}>
      <FlatList
        data={items}
        keyExtractor={item => item.id}
        renderItem={({ item }) => (
          <View style={styles.item}>
            <Text style={styles.name}>{item.name}</Text>
            <Text style={styles.price}>${(item.price * item.quantity).toFixed(2)}</Text>
            <TouchableOpacity onPress={() => removeItem(item.id)}>
              <Text style={styles.remove}>Remove</Text>
            </TouchableOpacity>
          </View>
        )}
      />
      <Text style={styles.total}>Total: ${total().toFixed(2)}</Text>
    </View>
  );
}

State management comparison

Solution Complexity Best for
useState Minimal Local component state
useContext + useState Low Auth, theme, small global state
Zustand Low-medium Most production apps
Redux Toolkit Medium-high Large teams, complex state
Jotai Low Atomic state, Next.js
TanStack Query Medium Server state, caching

11. Device features

Camera (expo-camera)

npx expo install expo-camera
import { useState, useRef } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';

export default function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const cameraRef = useRef<CameraView>(null);

  if (!permission) return <View />;

  if (!permission.granted) {
    return (
      <View style={styles.container}>
        <Text>Camera access is required.</Text>
        <TouchableOpacity style={styles.button} onPress={requestPermission}>
          <Text style={styles.buttonText}>Grant Permission</Text>
        </TouchableOpacity>
      </View>
    );
  }

  async function takePhoto() {
    const photo = await cameraRef.current?.takePictureAsync();
    console.log('Photo URI:', photo?.uri);
  }

  return (
    <View style={styles.container}>
      <CameraView style={styles.camera} ref={cameraRef} facing="back">
        <TouchableOpacity style={styles.captureButton} onPress={takePhoto} />
      </CameraView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  camera: { flex: 1 },
  captureButton: {
    position: 'absolute', bottom: 40, alignSelf: 'center',
    width: 72, height: 72, borderRadius: 36,
    backgroundColor: 'white', borderWidth: 4, borderColor: '#ddd',
  },
  button: { backgroundColor: '#6200ea', padding: 12, borderRadius: 8 },
  buttonText: { color: 'white', fontWeight: '600' },
});

Location (expo-location)

npx expo install expo-location
import { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import * as Location from 'expo-location';

export default function LocationScreen() {
  const [location, setLocation] = useState<Location.LocationObject | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function getLocation() {
    const { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== 'granted') {
      setError('Location permission denied.');
      return;
    }
    const loc = await Location.getCurrentPositionAsync({
      accuracy: Location.Accuracy.High,
    });
    setLocation(loc);
  }

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      {location && (
        <Text>
          {location.coords.latitude.toFixed(6)}, {location.coords.longitude.toFixed(6)}
        </Text>
      )}
      {error && <Text style={{ color: 'red' }}>{error}</Text>}
      <TouchableOpacity
        onPress={getLocation}
        style={{ backgroundColor: '#6200ea', padding: 16, borderRadius: 8, marginTop: 16 }}
      >
        <Text style={{ color: 'white', fontWeight: '600' }}>Get My Location</Text>
      </TouchableOpacity>
    </View>
  );
}

Push notifications (expo-notifications)

npx expo install expo-notifications
import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
    shouldShowBanner: true,
    shouldShowList: true,
  }),
});

export async function registerForPushNotifications() {
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') return null;

  const token = await Notifications.getExpoPushTokenAsync();
  return token.data;
}

// Schedule a local notification
await Notifications.scheduleNotificationAsync({
  content: {
    title: 'Reminder',
    body: 'Time to check the app!',
    data: { screen: 'Home' },
  },
  trigger: {
    type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
    seconds: 60,
  },
});

12. Building a complete app — Todo list

Here's a complete mini-app combining everything you've learned:

// App.tsx — Complete Todo App
import { useState } from 'react';
import {
  View, Text, TextInput, TouchableOpacity, FlatList,
  StyleSheet, SafeAreaView, Keyboard
} from 'react-native';

interface Todo {
  id: string;
  text: string;
  done: boolean;
}

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');

  function addTodo() {
    const text = input.trim();
    if (!text) return;
    setTodos(prev => [{ id: Date.now().toString(), text, done: false }, ...prev]);
    setInput('');
    Keyboard.dismiss();
  }

  function toggleTodo(id: string) {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }

  function deleteTodo(id: string) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }

  const remaining = todos.filter(t => !t.done).length;

  return (
    <SafeAreaView style={styles.safe}>
      <View style={styles.container}>
        <Text style={styles.title}>My Todos</Text>
        <Text style={styles.subtitle}>{remaining} remaining</Text>

        <View style={styles.inputRow}>
          <TextInput
            style={styles.input}
            value={input}
            onChangeText={setInput}
            placeholder="Add a todo..."
            onSubmitEditing={addTodo}
            returnKeyType="done"
          />
          <TouchableOpacity style={styles.addButton} onPress={addTodo}>
            <Text style={styles.addButtonText}>+</Text>
          </TouchableOpacity>
        </View>

        <FlatList
          data={todos}
          keyExtractor={item => item.id}
          renderItem={({ item }) => (
            <View style={styles.todoItem}>
              <TouchableOpacity
                style={[styles.checkbox, item.done && styles.checkboxDone]}
                onPress={() => toggleTodo(item.id)}
              >
                {item.done && <Text style={styles.checkmark}>✓</Text>}
              </TouchableOpacity>
              <Text style={[styles.todoText, item.done && styles.todoTextDone]}>
                {item.text}
              </Text>
              <TouchableOpacity onPress={() => deleteTodo(item.id)}>
                <Text style={styles.deleteButton}>✕</Text>
              </TouchableOpacity>
            </View>
          )}
          ListEmptyComponent={
            <Text style={styles.empty}>No todos yet. Add one above!</Text>
          }
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safe: { flex: 1, backgroundColor: '#f5f5f5' },
  container: { flex: 1, padding: 24 },
  title: { fontSize: 32, fontWeight: 'bold', color: '#1a1a1a' },
  subtitle: { fontSize: 14, color: '#666', marginBottom: 24 },
  inputRow: { flexDirection: 'row', marginBottom: 16, gap: 8 },
  input: {
    flex: 1,
    backgroundColor: 'white',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  addButton: {
    backgroundColor: '#6200ea',
    width: 48, height: 48,
    borderRadius: 8,
    alignItems: 'center', justifyContent: 'center',
  },
  addButtonText: { color: 'white', fontSize: 28, fontWeight: '300' },
  todoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'white',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
    gap: 12,
  },
  checkbox: {
    width: 24, height: 24, borderRadius: 12,
    borderWidth: 2, borderColor: '#6200ea',
    alignItems: 'center', justifyContent: 'center',
  },
  checkboxDone: { backgroundColor: '#6200ea', borderColor: '#6200ea' },
  checkmark: { color: 'white', fontSize: 14, fontWeight: 'bold' },
  todoText: { flex: 1, fontSize: 16, color: '#1a1a1a' },
  todoTextDone: { textDecorationLine: 'line-through', color: '#999' },
  deleteButton: { fontSize: 18, color: '#ccc', padding: 4 },
  empty: { textAlign: 'center', color: '#999', marginTop: 48, fontSize: 16 },
});

13. Publishing your app

Using Expo Application Services (EAS)

# Install EAS CLI
npm install -g eas-cli

# Login to your Expo account
eas login

# Configure builds
eas build:configure

# Build for iOS (requires Apple Developer account - $99/year)
eas build --platform ios

# Build for Android
eas build --platform android

# Submit to stores
eas submit --platform ios
eas submit --platform android

eas.json configuration

{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {}
  }
}

Pre-launch checklist

Item iOS Android
App icon (1024×1024 PNG) Required Required
Splash screen Required Required
Bundle ID / Package name com.company.appname com.company.appname
Developer account Apple ($99/year) Google ($25 one-time)
Privacy policy URL Required Required
Screenshots 3–10 per device size 2–8 per screen size
App description ≤4000 chars ≤4000 chars
Content rating Self-rated IARC questionnaire

14. Common patterns and best practices

Custom hook for API data

// hooks/useFetch.ts
import { useEffect, useState, useCallback } from 'react';

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetch_ = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setData(await res.json());
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, [url]);

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

  return { data, loading, error, refetch: fetch_ };
}

// Usage
const { data: posts, loading, error, refetch } = useFetch<Post[]>(
  'https://jsonplaceholder.typicode.com/posts?_limit=10'
);

Safe area handling

import { SafeAreaView } from 'react-native-safe-area-context';

// Always wrap your root screen with SafeAreaView
// to avoid content appearing behind the notch or home indicator
export default function Screen() {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
      {/* screen content */}
    </SafeAreaView>
  );
}

Platform-specific code

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

const styles = StyleSheet.create({
  shadow: {
    // iOS shadow
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    // Android shadow
    elevation: 4,
  },
  text: {
    fontFamily: Platform.select({
      ios: 'SF Pro Display',
      android: 'Roboto',
      default: 'System',
    }),
  },
});

// Platform-specific files
// Button.ios.tsx  → loaded on iOS
// Button.android.tsx → loaded on Android
// Button.tsx → fallback

Common mistakes

Mistake Problem Fix
Forgetting key in lists Performance warnings, wrong updates Always provide unique keyExtractor
Using <div> / <p> Crashes in React Native Use <View> / <Text>
Text outside <Text> Runtime error on Android Wrap all text in <Text>
Mutating state directly UI doesn't update Use setter function from useState
No error handling in async Silent failures Always try/catch async operations
Ignoring safe areas Content behind notch Use SafeAreaView
ScrollView with FlatList Performance issues Use FlatList alone for large lists
Missing permissions check App crashes Always request and check permissions

React Native vs alternatives

Comparison React Native Flutter Expo + RN Native
Language JavaScript/TypeScript Dart JavaScript/TypeScript Swift/Kotlin
Learning curve Low (if you know React) Medium Very low High
Performance Near-native Near-native Near-native Native
Code sharing ~95% ~95% ~95% 0%
Ecosystem Huge (npm) Growing Huge (npm) Platform-specific
Hot reload Yes Yes Yes Limited
Native modules Yes Yes (FFI) Via dev client Yes
Web support Limited (react-native-web) Yes Limited No

Learning path after this tutorial

Next step Resource
TypeScript in React Native Add tsconfig.json, type your components
TanStack Query Server state management, caching
Reanimated Smooth animations and gestures
Expo Router File-based navigation (like Next.js)
Testing Jest + React Native Testing Library
Performance Flashlist, memo, useCallback
Real-time WebSockets, Firebase Realtime DB
Offline-first WatermelonDB, MMKV

FAQ

Do I need a Mac to build React Native apps? For iOS development and App Store publishing, yes — you need a Mac with Xcode (or use EAS Build cloud service). Android works on any OS.

Expo vs React Native CLI — which should I pick? Start with Expo. It handles setup, over-the-air updates, and cloud builds. Switch to the bare workflow only when you need a native module that Expo doesn't support.

Is React Native fast enough for production? Yes. Apps like Facebook, Instagram, Shopify, and Microsoft Teams use React Native. Performance bottlenecks are rare and usually fixable with Reanimated, Flashlist, or memoization.

Do I need to know React first? It helps but isn't mandatory. This tutorial assumes basic JavaScript. Learning React concepts (components, props, state, hooks) alongside React Native works well.

How do I handle different screen sizes? Use percentage widths (width: '80%'), Dimensions.get('window'), useWindowDimensions(), and Flexbox. Avoid hardcoded pixel values.

What's the difference between TouchableOpacity and Pressable? Pressable is newer (React Native 0.64+), more flexible with pressed state callbacks, and supports hitSlop. Use Pressable for new projects; TouchableOpacity still works fine.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools