TL;DR: React Native lets you build real iOS and Android apps using JavaScript and React — the same skills your AI already uses for web apps. Expo is the toolkit that handles the painful parts (building, deploying, accessing device features). Together, they're the fastest path from "I have a web app idea" to "it's on the App Store." AI tools like Cursor and Claude Code generate React Native code well, but you need to know the gotchas.
Why AI Coders Need This
You've been building web apps with AI. Maybe a todo app, a dashboard, or a landing page. They work great in a browser. But your users want an app — a real app, on their phone, with push notifications and a home screen icon.
The traditional path to mobile development means learning Swift for iOS and Kotlin for Android — two completely different languages, two separate codebases, two different deployment processes. That's a non-starter for most vibe coders.
React Native solves this. You write one codebase in JavaScript/TypeScript using React components, and it compiles to actual native apps on both platforms. Not a website wrapped in an app frame — real native apps with native performance, native UI elements, and native capabilities.
The best part for AI-assisted developers: AI tools have extensive React Native training data. Ask Cursor or Claude Code to build you a mobile app, and they'll generate competent React Native code. But the code looks different from web React, and understanding why saves you hours of debugging.
The Real Scenario
"Build me a mobile app that shows a list of tasks with the ability to add, complete, and delete them. Use React Native with Expo. Include local storage so tasks persist between sessions."
Your AI generates something that looks familiar — but also subtly different from every web project you've built:
What AI Generated
// App.tsx — generated by AI
import { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
StyleSheet,
StatusBar,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface Task {
id: string;
text: string;
completed: boolean;
}
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [newTask, setNewTask] = useState('');
useEffect(() => {
loadTasks();
}, []);
useEffect(() => {
saveTasks();
}, [tasks]);
const loadTasks = async () => {
const stored = await AsyncStorage.getItem('tasks');
if (stored) setTasks(JSON.parse(stored));
};
const saveTasks = async () => {
await AsyncStorage.setItem('tasks', JSON.stringify(tasks));
};
const addTask = () => {
if (!newTask.trim()) return;
setTasks(prev => [...prev, {
id: Date.now().toString(),
text: newTask.trim(),
completed: false
}]);
setNewTask('');
};
const toggleTask = (id: string) => {
setTasks(prev =>
prev.map(t => t.id === id
? { ...t, completed: !t.completed }
: t
)
);
};
const deleteTask = (id: string) => {
setTasks(prev => prev.filter(t => t.id !== id));
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<Text style={styles.title}>My Tasks</Text>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
value={newTask}
onChangeText={setNewTask}
placeholder="Add a task..."
placeholderTextColor="#999"
onSubmitEditing={addTask}
/>
<TouchableOpacity style={styles.addButton} onPress={addTask}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<FlatList
data={tasks}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.taskRow}>
<TouchableOpacity onPress={() => toggleTask(item.id)}>
<Text style={[
styles.taskText,
item.completed && styles.taskCompleted
]}>
{item.completed ? '✅' : '⬜'} {item.text}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => deleteTask(item.id)}>
<Text style={styles.deleteText}>🗑️</Text>
</TouchableOpacity>
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#1a1a2e', padding: 20, paddingTop: 60 },
title: { fontSize: 28, fontWeight: 'bold', color: '#fff', marginBottom: 20 },
inputRow: { flexDirection: 'row', marginBottom: 20, gap: 10 },
input: {
flex: 1, backgroundColor: '#16213e', color: '#fff',
padding: 12, borderRadius: 8, fontSize: 16
},
addButton: {
backgroundColor: '#0f3460', padding: 12, borderRadius: 8,
justifyContent: 'center', alignItems: 'center', width: 48
},
addButtonText: { color: '#fff', fontSize: 24, fontWeight: 'bold' },
taskRow: {
flexDirection: 'row', justifyContent: 'space-between',
alignItems: 'center', padding: 12, backgroundColor: '#16213e',
borderRadius: 8, marginBottom: 8
},
taskText: { color: '#fff', fontSize: 16, flex: 1 },
taskCompleted: { textDecorationLine: 'line-through', color: '#666' },
deleteText: { fontSize: 20, marginLeft: 10 },
});
If you've worked with React for the web, a lot of this looks familiar. useState, useEffect, component functions, props. But look closer — no <div>, no <p>, no <input>. No CSS file. No className. What's going on?
Understanding Each Part
The Core Swap: Web Elements → Native Components
This is the biggest mental shift. In web React, you use HTML elements. In React Native, you use platform-specific components that render to actual native views:
| Web React | React Native | What It Becomes |
|---|---|---|
<div> | <View> | UIView (iOS) / android.view.View (Android) |
<p>, <span> | <Text> | UILabel (iOS) / TextView (Android) |
<input> | <TextInput> | UITextField (iOS) / EditText (Android) |
<button> | <TouchableOpacity> | Native touch handler with opacity feedback |
<ul> with .map() | <FlatList> | Optimized native scroll view with recycling |
<img> | <Image> | UIImageView (iOS) / ImageView (Android) |
<scroll-div> | <ScrollView> | UIScrollView (iOS) / ScrollView (Android) |
This is why your AI's mobile code looks unfamiliar. It's not HTML anymore — these are direct mappings to the platform's native UI toolkit. That's what makes React Native apps feel like "real" apps instead of web pages in a wrapper.
StyleSheet Instead of CSS
There's no CSS file. Instead, React Native uses StyleSheet.create() — a JavaScript object that looks like CSS but isn't quite:
// Web CSS
.container {
display: flex;
flex-direction: column;
background-color: #1a1a2e;
padding: 20px;
}
// React Native StyleSheet
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column', // This is actually the default
backgroundColor: '#1a1a2e',
padding: 20, // No 'px' — units are density-independent pixels
}
});
Key differences: no px units (everything is density-independent pixels by default), camelCase property names (not kebab-case), and flexDirection: 'column' is the default (opposite of web CSS where it's row). AI usually handles this correctly, but watch for web CSS habits leaking in.
AsyncStorage Instead of localStorage
On the web, you'd use localStorage to persist data. Mobile apps don't have that — they use AsyncStorage, which works similarly but is asynchronous (requires await). Your AI knows this swap, but sometimes generates localStorage by habit if it loses context about the platform.
FlatList Instead of .map()
On the web, you'd render a list with {items.map(item => ...)}. React Native's FlatList does the same thing but with a critical difference: it only renders items that are currently visible on screen. For a list of 10 tasks, this doesn't matter. For a list of 10,000 items, it's the difference between a smooth app and a crash.
Expo: The Part That Makes This Actually Possible
Here's a secret about React Native: building and deploying apps without Expo is genuinely painful. You need Xcode, Android Studio, CocoaPods, Gradle, platform-specific signing certificates — a minefield of configuration that has nothing to do with writing code.
Expo handles all of that. It's a platform built on top of React Native that gives you:
- Expo Go: A phone app that lets you preview your project instantly — scan a QR code, see your app running. No build step needed during development.
- EAS Build: Cloud builds for iOS and Android. You don't need a Mac to build an iOS app — Expo's servers handle it.
- EAS Submit: Submits your app directly to the App Store and Google Play.
- Expo SDK: Pre-built modules for camera, notifications, location, biometrics, file system — things that would require native code in bare React Native.
- Over-the-Air Updates: Push bug fixes without going through the App Store review process.
When you ask AI to build a mobile app, say: "Use Expo with the latest SDK." This ensures AI generates the Expo-managed workflow, which is dramatically simpler than bare React Native. If AI generates react-native init instead of npx create-expo-app, redirect it.
Getting Started with Expo
# Create a new Expo project
npx create-expo-app@latest my-app --template blank-typescript
# Start the dev server
cd my-app
npx expo start
# Scan the QR code with Expo Go on your phone
# Or press 'i' for iOS simulator / 'a' for Android emulator
That's it. Three commands and you have a mobile app running on your phone. Compare that to the bare React Native setup which involves Xcode configuration, CocoaPods, Android SDK setup, and at least 30 minutes of environment troubleshooting.
React Native vs Flutter: Which Should You Pick?
This is the question every vibe coder asks when going mobile. Here's the honest comparison:
| Factor | React Native + Expo | Flutter |
|---|---|---|
| Language | JavaScript / TypeScript | Dart |
| Learning curve for web devs | Low — React skills transfer | Medium — new language + widget system |
| AI code generation quality | Excellent — massive training data | Good — less training data than RN |
| Hot reload speed | Fast | Fast (slightly faster) |
| Native look & feel | Uses actual native components | Custom-rendered (looks the same on both platforms) |
| Package ecosystem | Huge (npm) | Growing (pub.dev) |
| Web + mobile from one codebase | Possible but limited | Better web support |
| Complex animations | Good (Reanimated library) | Excellent (built-in) |
| App size | ~10-15 MB | ~15-20 MB |
| Companies using it | Meta, Microsoft, Shopify, Discord | Google, BMW, eBay, Alibaba |
The vibe coder verdict: If you're coming from web development and already use React, go with React Native + Expo. Your JavaScript skills transfer directly, the npm ecosystem is familiar, and AI generates better React Native code because there's more of it in training data. Choose Flutter only if you're starting completely fresh and want the most consistent cross-platform look.
What AI Gets Wrong About React Native
The most common AI mistake: it forgets it's writing for mobile and generates <div>, <span>, and CSS classes. This happens when your conversation switches between web and mobile topics. Fix: Start mobile prompts with "In my React Native / Expo project..." to anchor the context.
AI often generates React Router (a web library) instead of React Navigation (the standard mobile library). Or it mixes them. Mobile navigation is fundamentally different — you have stacks, tabs, and drawers, not URL routes. Fix: Specify "Use @react-navigation/native with a native stack navigator" in your prompt.
Some features work differently on iOS and Android. The status bar, safe area insets, back button behavior, and permission flows are all platform-specific. AI often generates code that works on one platform and breaks on the other. Fix: Always test on both platforms. Use Platform.OS checks and SafeAreaView from the start.
Expo moves fast. AI might generate code using expo install (deprecated) instead of npx expo install, or reference SDK 48 APIs that changed in SDK 52. Import paths change between versions. Fix: Tell AI your exact Expo SDK version: "I'm using Expo SDK 52."
Not every npm package works in React Native. AI might suggest a web-only package (like anything that uses the DOM) without warning you. Fix: Before installing any AI-suggested package, check if it says "React Native" in its docs. Or ask AI directly: "Does [package] work in React Native?"
Expo Managed vs Bare React Native
You'll see people online debating "Expo vs React Native" — but this is a false choice. Expo IS React Native with extra tools. The real question is: Expo managed workflow or bare workflow?
| Feature | Expo Managed | Bare React Native |
|---|---|---|
| Setup time | 2 minutes | 30+ minutes |
| Xcode/Android Studio needed | No (for development) | Yes, always |
| Cloud builds | Yes (EAS Build) | Manual setup required |
| OTA updates | Built-in | Manual (CodePush or custom) |
| Custom native code | Yes (via config plugins) | Yes (direct access) |
| Any npm native module | Most (via Expo Modules) | All |
| Recommended for vibe coders | Yes | Only if you need very specific native APIs |
The rule of thumb: Start with Expo managed. If you hit a wall where you need a native module Expo doesn't support, you can eject to bare. But most vibe coders never need to.
Debugging React Native with AI
Mobile debugging is different from web debugging. Here's what to tell your AI when things break:
Red Screen of Death
React Native shows a full-screen red error when something crashes. Copy the entire error message and paste it to your AI. These are usually clear: "Cannot read property 'X' of undefined" or "Invariant Violation: Text strings must be rendered within a <Text> component."
Yellow Warnings
Yellow boxes are warnings, not errors. They won't crash your app but might indicate problems. AI sometimes generates code that triggers deprecation warnings — tell it to fix them before they become errors in the next SDK version.
The "It Works on iOS but Not Android" Problem
Tell your AI: "This works on iOS but crashes/looks wrong on Android. Here's the component: [paste code]. What's platform-specific here?" AI is actually good at spotting platform differences when you explicitly ask.
Build Failures
EAS Build failures dump logs. The relevant part is usually at the end. Copy the last 20-30 lines of the build log and paste them to your AI. Common causes: incompatible native module versions, missing Expo config plugin, or iOS code signing issues.
Install React DevTools and connect it to your Expo app. You can inspect component trees, state, and props — just like web React. Run npx react-devtools and it connects automatically to Expo Go.
Frequently Asked Questions
For Android apps, no — you can develop on Windows, Mac, or Linux. For iOS apps, you need a Mac to build the final app for the App Store. However, with Expo Go, you can preview your iOS app on a physical iPhone during development from any operating system. Many vibe coders develop on Windows and only touch a Mac (or use EAS Build cloud service) when it's time to ship to the App Store.
React Native is the framework — it's the technology that turns JavaScript into native mobile apps. Expo is a platform built on top of React Native that handles the hard parts: building, deploying, updating, and accessing device features. Think of React Native as the engine and Expo as the car. You can drive the engine directly (bare React Native), but most people — especially vibe coders — prefer the car.
Your JavaScript logic, state management, and API calls transfer directly. Your UI components do not — React Native uses View, Text, and ScrollView instead of div, p, and section. CSS doesn't work the same way either. AI tools handle this translation well, but expect your UI layer to be rewritten even if your business logic stays the same.
For most apps, yes. React Native renders actual native components — not a web view — so scrolling, animations, and touch responses feel native. The performance gap has narrowed dramatically with the New Architecture (Fabric renderer and TurboModules). Heavy 3D games or complex real-time animations might still need fully native code, but 95% of apps run perfectly fine on React Native.
If you already know JavaScript or React from building web apps with AI, React Native is the obvious choice — your skills transfer directly. Flutter uses Dart, which means learning a new language. Flutter has slightly better performance for complex animations and a more consistent look across platforms. But for vibe coders coming from web development, React Native with Expo gets you to a working mobile app faster.