Articles › Mobile & Desktop

Getting Started with React Native and Expo: Build Your First Mobile App

Build your first mobile app with React Native and Expo: core components, styling, Expo Router navigation, calling an API, RTL support and publishing.

If you know React, you're most of the way to building native mobile apps. React Native lets you write iOS and Android apps with the same components, hooks and state management you use on the web — and the result renders real native UI, not a web view. With Expo, setup takes minutes. This guide walks through the essentials of building a first real app.

How React Native works

You write React components in JavaScript/TypeScript. Instead of rendering <div> and <span> to the DOM, React Native renders native views: <View> becomes an Android ViewGroup or iOS UIView, <Text> a native text view. Since the New Architecture (Fabric and TurboModules, default in recent versions), JavaScript talks to native code through a fast, synchronous interface (JSI) instead of a JSON bridge.

What you share with React web: components, hooks, context, React Query, Zustand/Redux, validation libraries, most business logic. What's different: no HTML or CSS files, different components, and platform APIs (camera, notifications, storage).

Setup with Expo

npx create-expo-app@latest shop-app
cd shop-app
npx expo start

Install Expo Go on your phone and scan the QR code — the app runs on your device instantly, with hot reload as you save. For features that need custom native code, you later create a development build, but Expo Go is perfect for learning.

Tip: Expo is the officially recommended way to start a React Native project. It doesn't lock you in: you can add any native library and still build with the standard tooling.

Core components

Web React Native Notes
<div> <View> layout container
<span>, <p> <Text> all text must be inside <Text>
<img> <Image> (or expo-image) needs explicit size for remote images
<input> <TextInput> onChangeText instead of onChange
<button> <Pressable> full control over pressed styles
scrolling div <ScrollView> for short content
long lists <FlatList> virtualized; use for anything long
import { View, Text, Pressable, StyleSheet } from "react-native";

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View style={styles.card}>
      <Text style={styles.title}>Count: {count}</Text>
      <Pressable
        onPress={() => setCount((c) => c + 1)}
        style={({ pressed }) => [styles.button, pressed && { opacity: 0.7 }]}
      >
        <Text style={styles.buttonText}>Add one</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 20, borderRadius: 16, backgroundColor: "#fff", gap: 12 },
  title: { fontSize: 20, fontWeight: "600", color: "#13121f" },
  button: { backgroundColor: "#7c3aed", paddingVertical: 12, borderRadius: 12, alignItems: "center" },
  buttonText: { color: "#fff", fontWeight: "600" },
});

Styling and layout

  • Styles are JavaScript objects with camelCase properties (backgroundColor, marginTop).
  • Units are density-independent pixels — no px, rem or % for most properties (percentages work for width/height).
  • Flexbox is the layout system, but flexDirection defaults to column.
  • No cascading: styles don't inherit, except text styles within nested <Text>.
  • Use SafeAreaView (from react-native-safe-area-context) so content avoids the notch and home indicator.

If you love Tailwind, NativeWind brings utility classes to React Native.

Lists done right

<FlatList
  data={products}
  keyExtractor={(item) => String(item.id)}
  renderItem={({ item }) => <ProductRow product={item} />}
  ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
  refreshing={isRefreshing}
  onRefresh={refresh}
  ListEmptyComponent={<Text>No products found.</Text>}
/>

FlatList renders only visible rows, supports infinite scroll and pull-to-refresh out of the box. Never .map() hundreds of items inside a ScrollView.

Expo Router uses file-based routing, similar to Next.js:

app/
  _layout.jsx          → root stack
  (tabs)/
    _layout.jsx        → bottom tabs
    index.jsx          → Home tab
    orders.jsx         → Orders tab
  product/[id].jsx     → product details
// app/(tabs)/_layout.jsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";

export default function TabsLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: "#7c3aed" }}>
      <Tabs.Screen name="index" options={{ title: "Home", tabBarIcon: ({ color }) => <Ionicons name="home" size={22} color={color} /> }} />
      <Tabs.Screen name="orders" options={{ title: "Orders", tabBarIcon: ({ color }) => <Ionicons name="receipt" size={22} color={color} /> }} />
    </Tabs>
  );
}
// app/product/[id].jsx
import { useLocalSearchParams, Stack } from "expo-router";

export default function ProductScreen() {
  const { id } = useLocalSearchParams();
  const { data } = useProduct(id);
  return (
    <>
      <Stack.Screen options={{ title: data?.name ?? "Product" }} />
      <ProductDetails product={data} />
    </>
  );
}

// navigate from anywhere
<Link href={`/product/${item.id}`}>View</Link>

Routes are also deep links (shopapp://product/42) automatically.

Calling your ASP.NET Core API

fetch works exactly like on the web; React Query adds caching, retries and loading states:

const API = process.env.EXPO_PUBLIC_API_URL;   // e.g. https://api.example.com

export function useProducts() {
  return useQuery({
    queryKey: ["products"],
    queryFn: async () => {
      const res = await fetch(`${API}/api/products`);
      if (!res.ok) throw new Error("Failed to load products");
      return res.json();
    },
  });
}

Things that differ from the web:

  • CORS doesn't apply — mobile apps aren't browsers. Your API still needs proper authentication.
  • localhost on a phone means the phone. During development use your computer's LAN IP (http://192.168.1.10:5179) and allow HTTP for development only.
  • Production APIs must use HTTPS (iOS blocks plain HTTP by default).
  • Handle offline and slow networks: show cached data, retry, and give clear error states.

Storing data and tokens

  • Secrets (access/refresh tokens): expo-secure-store — uses the iOS Keychain and Android Keystore.
  • Preferences and small data: @react-native-async-storage/async-storage.
  • Larger structured data / offline: expo-sqlite.
import * as SecureStore from "expo-secure-store";

await SecureStore.setItemAsync("refreshToken", token);
const saved = await SecureStore.getItemAsync("refreshToken");

Warning: Never store tokens in AsyncStorage — it's unencrypted. And never embed API secrets in the app bundle; anything shipped in the app can be extracted.

RTL and Persian apps

React Native supports right-to-left layouts through I18nManager:

import { I18nManager } from "react-native";

I18nManager.allowRTL(true);
I18nManager.forceRTL(true);   // takes effect after the app reloads

With RTL enabled, flexDirection: "row" flows right-to-left and marginStart/paddingEnd follow the direction — prefer them over marginLeft/marginRight. Load a Persian font (for example Vazirmatn) with expo-font and apply it to your Text components, and use writingDirection: "rtl" for mixed-direction text.

Native features with Expo modules

Expo provides well-maintained modules for common needs: expo-camera, expo-image-picker, expo-location, expo-notifications, expo-local-authentication (fingerprint/Face ID), expo-haptics. Always request permissions with a clear explanation, and handle the "denied" case gracefully.

Building and publishing

EAS (Expo Application Services) builds signed binaries in the cloud — you don't even need a Mac to build for iOS:

npm install -g eas-cli
eas login
eas build:configure
eas build --platform android      # .aab for Google Play (or apk for direct install)
eas build --platform ios
eas submit --platform android
  • Android: Google Play developer account; for Iranian users, also consider Cafe Bazaar and Myket, which accept the same APK/AAB.
  • iOS: Apple Developer Program membership.
  • Over-the-air updates (eas update) push JavaScript fixes to users without a new store release — for JS changes only.

Performance tips

  • Use FlatList/FlashList for lists and memoize row components.
  • Avoid inline anonymous functions in hot list items when profiling shows re-render problems.
  • Use expo-image for caching and smooth image loading.
  • Run animations on the UI thread with react-native-reanimated.
  • Test release builds on a real mid-range Android device — dev mode is much slower than production.

Summary

React Native lets React developers build real native apps for iOS and Android. Start with Expo, learn the core components (View, Text, Pressable, FlatList), style with Flexbox and StyleSheet, navigate with Expo Router, call your API with fetch and React Query, keep tokens in SecureStore, and build and ship with EAS. Most of your React knowledge — and much of your web code — carries over directly.

FAQ

React Native or Flutter?

Both are excellent. React Native is the natural choice if your team already knows React and JavaScript/TypeScript and wants to share logic with a web app; Flutter uses Dart and its own rendering engine.

Can I share code between my React website and React Native app?

Yes — business logic, API clients, validation schemas, hooks and state stores share well. UI components usually differ, unless you use a cross-platform UI layer like React Native Web.

Do I need to know Java, Kotlin or Swift?

Not to start. Expo modules cover most needs. Native knowledge helps when you write custom native modules or debug platform-specific build issues.