React Native Fundamentals: Complete Guide
From environment setup to your first complete app — components, Flexbox, hooks, React Navigation, REST APIs, AsyncStorage, and cross-platform patterns with flowcharts and code examples.
What is React Native?
React Native is an open-source framework by Meta that lets you build native mobile apps using JavaScript and React. You write one codebase and deploy to both iOS and Android — each platform renders real native UI components, not a WebView.
Unlike hybrid apps (Cordova, Ionic WebView), React Native maps your <View> to UIView on iOS and android.view on Android. Users get native scroll physics, accessibility, and performance.
React Native is used by Facebook, Instagram, Shopify, Discord, Microsoft, and thousands of startups. It is ideal when you already know React and need mobile + web skill overlap.
How React Native works
Your JavaScript runs on Hermes (or JavaScriptCore) inside the app. React components describe UI; the bridge serializes updates to native views. New Architecture (Fabric + TurboModules) reduces bridge overhead for better performance.
React Native vs alternatives
- Flutter — Dart language, own rendering engine; excellent performance, smaller talent pool.
- Expo — toolchain on top of RN; faster setup, managed builds; can eject to bare RN.
- Native (Swift/Kotlin) — maximum control; two separate codebases.
- React Native — best when team knows React; huge npm ecosystem; OTA updates possible.
What you will build
- Understand project structure, Metro bundler, and dev workflow.
- Build screens with core components, Flexbox styling, and lists.
- Manage state with hooks and Context; navigate with React Navigation.
- Fetch REST APIs, cache with AsyncStorage, handle loading and errors.
- Ship a complete cross-platform app: auth flow, tabs, and API-driven screens.
Prerequisites
You need Node.js 18+, a code editor (VS Code recommended), and platform SDKs for the devices you target. macOS is required for iOS Simulator; Android emulator runs on Windows, macOS, and Linux.
- Node.js 18 LTS or 20 LTS — use nvm to manage versions.
- Watchman (macOS) — file watcher for Metro: brew install watchman.
- Xcode 15+ (macOS only) — iOS Simulator and App Store builds.
- Android Studio — SDK, emulator, and platform tools.
- JDK 17 — required by modern Android Gradle Plugin.
Create your first project
npx @react-native-community/cli@latest init MyFirstApp
cd MyFirstApp
# iOS (macOS only)
cd ios && pod install && cd ..
npx react-native run-ios
# Android
npx react-native run-androidnpx create-expo-app@latest MyFirstApp
cd MyFirstApp
npx expo start
# Press i for iOS simulator, a for Android emulatorVerify your setup
- Metro bundler starts without errors on npx react-native start.
- iOS Simulator or Android Emulator launches the default welcome screen.
- Edit App.tsx — save — app hot-reloads within 2 seconds.
- React Native Doctor: npx react-native doctor — fixes common issues.
Key folders in a bare React Native project
- App.tsx / index.js — JavaScript entry point registered with AppRegistry.
- src/ — your components, screens, hooks, services (convention, not required).
- android/ — Gradle project, AndroidManifest.xml, native Java/Kotlin code.
- ios/ — Xcode workspace, Info.plist, native Swift/Objective-C code.
- node_modules/ — npm dependencies.
- metro.config.js — bundler configuration (assets, transformers).
Metro Bundler explained
Metro is React Native's JavaScript bundler (like webpack for web). It transforms JSX/TypeScript, resolves imports, and serves the bundle to the app in development. In production, a single optimized bundle is embedded in the app.
Fast Refresh preserves component state when you edit code — essential for rapid UI iteration.
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);Recommended src/ structure
src/
components/ # reusable UI (Button, Card)
screens/ # full pages (HomeScreen, LoginScreen)
navigation/ # React Navigation config
hooks/ # custom hooks (useAuth, useFetch)
services/ # API clients, storage helpers
context/ # React Context providers
types/ # TypeScript interfaces
utils/ # formatters, validatorsBuilding blocks of every screen
React Native provides primitive components that map to native widgets. You compose them like HTML divs and spans — but use <View> and <Text>, never raw strings outside <Text>.
Essential components
- View — container for layout (like div). Supports Flexbox, touch handling, accessibility.
- Text — all text must be inside Text; supports nesting and styling.
- Image — local (require) or remote (uri); always set dimensions or flex.
- ScrollView — scrollable container for content that does not fit one screen.
- SafeAreaView — respects notch and home indicator on modern phones.
- StatusBar — control status bar style (light/dark content).
import { View, Text, Image, ScrollView, SafeAreaView } from "react-native";
export function ProfileScreen() {
return (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ padding: 16 }}>
<Image
source={{ uri: "https://example.com/avatar.jpg" }}
style={{ width: 80, height: 80, borderRadius: 40 }}
/>
<Text style={{ fontSize: 24, fontWeight: "bold" }}>Jane Developer</Text>
<Text style={{ color: "#666" }}>React Native engineer</Text>
</ScrollView>
</SafeAreaView>
);
}StyleSheet and Flexbox
React Native uses JavaScript objects for styles — no CSS files. Flexbox is the default layout system and works the same on iOS and Android. flexDirection defaults to column (unlike web row).
Key Flexbox properties
- flexDirection: 'row' | 'column' — main axis direction.
- justifyContent — align along main axis (center, space-between, flex-end).
- alignItems — align along cross axis.
- flex: 1 — grow to fill available space.
- gap — spacing between children (RN 0.71+).
import { StyleSheet, View, Text } from "react-native";
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: "#0f172a" },
card: {
backgroundColor: "#1e293b",
borderRadius: 12,
padding: 16,
marginBottom: 12,
},
title: { fontSize: 18, fontWeight: "600", color: "#f1f5f9" },
});
export function Card({ title }: { title: string }) {
return (
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
</View>
);
}Platform-specific styles
import { Platform, StyleSheet } from "react-native";
const styles = StyleSheet.create({
shadow: Platform.select({
ios: { shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2 },
android: { elevation: 4 },
}),
});Handling user interaction
Use Pressable (modern) or TouchableOpacity for buttons and tappable areas. TextInput handles keyboard input. Always provide accessibilityLabel for screen readers.
import { Pressable, Text, StyleSheet } from "react-native";
export function PrimaryButton({ title, onPress }: { title: string; onPress: () => void }) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.btn, pressed && styles.pressed]}
accessibilityRole="button"
accessibilityLabel={title}
>
<Text style={styles.label}>{title}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
btn: { backgroundColor: "#06b6d4", padding: 14, borderRadius: 10, alignItems: "center" },
pressed: { opacity: 0.85 },
label: { color: "#fff", fontWeight: "600" },
});const [email, setEmail] = useState("");
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
style={styles.input}
/>State and side effects in functional components
useState stores data that changes over time — taps, form input, API responses. useEffect runs side effects after render — fetching data, subscriptions, timers. Together they replace class component lifecycle methods.
useState patterns
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: "", age: 0 });
// Functional update — safe when new state depends on old
setCount((prev) => prev + 1);
// Object update — always spread, never mutate
setUser((prev) => ({ ...prev, name: "Alex" }));useEffect patterns
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch("https://api.example.com/posts");
const data = await res.json();
if (!cancelled) setPosts(data);
}
load();
return () => { cancelled = true; }; // cleanup on unmount
}, []); // empty deps = run once on mountRules of hooks
- Only call hooks at the top level — not inside loops, conditions, or nested functions.
- Only call hooks from React function components or custom hooks.
- Dependency array controls when useEffect re-runs — include all values used inside.
Building reusable UI
Props pass data and callbacks from parent to child. Composition — nesting children inside components — is preferred over inheritance. Build small, focused components and assemble screens from them.
type ProductCardProps = {
title: string;
price: number;
imageUrl: string;
onPress: () => void;
};
export function ProductCard({ title, price, imageUrl, onPress }: ProductCardProps) {
return (
<Pressable onPress={onPress} style={styles.card}>
<Image source={{ uri: imageUrl }} style={styles.image} />
<Text style={styles.title}>{title}</Text>
<Text style={styles.price}>₹{price}</Text>
</Pressable>
);
}export function Screen({ children }: { children: React.ReactNode }) {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.content}>{children}</View>
</SafeAreaView>
);
}Efficient long lists
Never map() thousands of items inside ScrollView — it renders everything at once and crashes. FlatList virtualizes: only visible rows mount. SectionList groups data with headers.
type Item = { id: string; title: string };
export function PostList({ data, onRefresh }: { data: Item[]; onRefresh: () => void }) {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = async () => {
setRefreshing(true);
await onRefresh();
setRefreshing(false);
};
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <PostCard title={item.title} />}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
ListEmptyComponent={<Text>No posts yet</Text>}
contentContainerStyle={{ padding: 16 }}
/>
);
}FlatList performance tips
- Always provide keyExtractor with stable unique IDs.
- Use getItemLayout when all rows have fixed height — skips measurement.
- Memoize renderItem with useCallback; wrap row component in React.memo.
- Avoid anonymous functions in renderItem when possible.
Building forms in React Native
Controlled inputs with useState work for simple forms. For complex forms use react-hook-form or Formik. Validate on submit and show inline errors.
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState<Record<string, string>>({});
function validate() {
const e: Record<string, string> = {};
if (!email.includes("@")) e.email = "Invalid email";
if (password.length < 6) e.password = "Min 6 characters";
setErrors(e);
return Object.keys(e).length === 0;
}
function handleSubmit() {
if (!validate()) return;
// call API...
}Keyboard handling
- KeyboardAvoidingView — shifts content when keyboard opens (behavior='padding' on iOS).
- keyboardShouldPersistTaps='handled' on ScrollView — taps work while keyboard is open.
- TouchableWithoutFeedback + Keyboard.dismiss() — tap outside to close keyboard.
Screen navigation fundamentals
React Navigation is the standard routing library for React Native. Stack Navigator pushes screens like a deck of cards — with back gestures on iOS and hardware back on Android.
Setup and basic stack
npm install @react-navigation/native @react-navigation/native-stack
npm install react-native-screens react-native-safe-area-context
# iOS: cd ios && pod installimport { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
export type RootStackParamList = {
Home: undefined;
Details: { itemId: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
// Navigate with typed params:
// navigation.navigate("Details", { itemId: "42" });Multi-section apps
Bottom tabs suit primary app sections (Home, Search, Profile). Drawer suits many secondary destinations. Nest navigators: tabs inside stack, or stack inside each tab.
Bottom tab navigator
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
const Tab = createBottomTabNavigator();
function MainTabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}Navigation patterns
- Auth flow: Stack with Login vs MainTabs based on isLoggedIn state.
- Modal screens: presentation: 'modal' in stack screen options.
- Deep linking: map URLs to screens for push notification taps.
- Type-safe params: define ParamList types for every navigator.
Sharing state across screens
Context passes data through the tree without prop drilling. Use for theme, auth user, and locale. For complex global state consider Zustand or Redux Toolkit — but Context is enough for most apps.
type AuthContextType = {
user: User | null;
login: (email: string, pass: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, pass: string) => {
const res = await api.login(email, pass);
setUser(res.user);
await AsyncStorage.setItem("token", res.token);
};
const logout = async () => {
setUser(null);
await AsyncStorage.removeItem("token");
};
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 inside AuthProvider");
return ctx;
}Networking in React Native
Use fetch (built-in) or axios for HTTP requests. Handle loading, success, and error states explicitly. Never assume the network is fast or available — mobile users switch between WiFi and cellular constantly.
API service layer
const BASE_URL = "https://api.example.com";
export async function apiGet<T>(path: string, token?: string): Promise<T> {
const res = await fetch(BASE_URL + path, {
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (!res.ok) throw new Error(`API error ${res.status}`);
return res.json();
}
// Usage in screen:
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
apiGet<Post[]>("/posts")
.then(setPosts)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, []);Best practices
- Centralize API calls in services/ — screens only call hooks.
- Use custom hook useFetch(url) to DRY loading/error logic.
- Set timeout with AbortController for slow networks.
- Show ActivityIndicator while loading; retry button on error.
Storing data on device
@react-native-async-storage/async-storage is an asynchronous, persistent key-value store. Use for auth tokens, user preferences, and offline cache. For sensitive data prefer react-native-keychain.
import AsyncStorage from "@react-native-async-storage/async-storage";
export async function saveToken(token: string) {
await AsyncStorage.setItem("auth_token", token);
}
export async function getToken() {
return AsyncStorage.getItem("auth_token");
}
export async function saveCache<T>(key: string, data: T) {
await AsyncStorage.setItem(key, JSON.stringify(data));
}
export async function loadCache<T>(key: string): Promise<T | null> {
const raw = await AsyncStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
}When to use what
- AsyncStorage — tokens, settings, small JSON cache (< 6MB per key on Android).
- Keychain / EncryptedStorage — passwords, refresh tokens, PII.
- SQLite / WatermelonDB — large structured offline datasets.
- MMKV — fast key-value alternative for performance-critical reads.
iOS vs Android differences
Most RN code is cross-platform. When platforms diverge — shadows, permissions, back button — use Platform API or separate .ios.tsx / .android.tsx files.
import { Platform } from "react-native";
const TAB_BAR_HEIGHT = Platform.OS === "ios" ? 88 : 64;
if (Platform.OS === "android") {
// Android-only logic
}Icon.ios.tsx → used on iOS only
Icon.android.tsx → used on Android only
Icon.tsx → fallback for bothCommon platform differences
- Shadows: shadow* props on iOS, elevation on Android.
- Back button: Android hardware back — handle with BackHandler or React Navigation.
- Safe areas: notches, Dynamic Island, navigation bar insets.
- Permissions: request camera/location at runtime with react-native-permissions.
Working with media
Bundle static images with require() — Metro includes them in the build. Remote images use uri but need explicit width/height. Use vector icons (@expo/vector-icons or react-native-vector-icons) for crisp icons at any size.
// Local — width/height optional (Metro knows dimensions)
<Image source={require("./assets/logo.png")} />
// Remote — always set dimensions
<Image
source={{ uri: "https://example.com/photo.jpg" }}
style={{ width: 300, height: 200 }}
resizeMode="cover"
/>
// Vector icon
import { Ionicons } from "@expo/vector-icons";
<Ionicons name="home" size={24} color="#06b6d4" />Finding and fixing bugs
- Metro terminal — redbox shows JS errors with stack trace; tap to open.
- React Native DevTools — press j in Metro or shake device → Debug.
- Flipper / Reactotron — network inspector, AsyncStorage viewer, layout.
- console.log — works in Metro terminal; remove before production.
- React DevTools — inspect component tree and props.
Common issues and fixes
- Red screen 'Unable to resolve module' — wrong import path or missing npm install.
- White screen — silent JS error; check Metro logs.
- Network request failed on Android emulator — use 10.0.2.2 instead of localhost.
- Pod install errors — cd ios && pod deintegrate && pod install.
- Clear cache: npx react-native start --reset-cache.
iOS Simulator: http://localhost:3000/api
Android Emulator: http://10.0.2.2:3000/api
Physical device: http://YOUR_LAN_IP:3000/apiProject: Rishtaara Reader app
Let's architect a complete cross-platform app: login screen, bottom tabs (Home + Profile), API-driven article list, pull-to-refresh, and persisted auth token. This ties together every concept in the course.
App structure
export default function App() {
return (
<AuthProvider>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</AuthProvider>
);
}
function RootNavigator() {
const { user, loading } = useAuth();
if (loading) return <SplashScreen />;
return user ? <MainTabs /> : <AuthStack />;
}export function HomeScreen() {
const { data, loading, error, refresh } = useArticles();
if (loading) return <ActivityIndicator size="large" />;
if (error) return <ErrorView message={error} onRetry={refresh} />;
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ArticleCard
title={item.title}
excerpt={item.excerpt}
onPress={() => navigation.navigate("Article", { id: item.id })}
/>
)}
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} />}
/>
);
}Checklist before calling it done
- Works on both iOS Simulator and Android Emulator.
- Loading, empty, and error states handled on every API screen.
- Keyboard does not cover login form inputs.
- Back navigation works correctly from detail screens.
- Token persists — app reopens logged in after kill.
You have the fundamentals — what comes next?
- Animations — React Native Reanimated for 60fps gestures and transitions.
- Native modules — bridge to Swift/Kotlin when RN APIs are not enough.
- TypeScript strict mode — catch bugs at compile time across large codebases.
- Testing — Jest + React Native Testing Library for unit; Detox for E2E.
- CI/CD — automate builds and store uploads (see our Advanced CI/CD course).
- Expo Router — file-based navigation if you use Expo SDK 50+.
Practice and continue learning
- MCQ Quiz: /mcq/react-native-fundamentals-mcq
- Interview prep: /interview/react-native-fundamentals-interview
- Advanced CI/CD course: /learn/mobile-development/react-native-advanced-ci-cd
- Full reference guide: /knowledge/react-native-fundamentals
Key Takeaways
- React Native renders real native UI from React components — one codebase for iOS and Android.
- Master View, Text, Flexbox, and FlatList before adding libraries.
- useState + useEffect + Context cover most app state needs.
- React Navigation Stack + Tabs is the standard routing pattern.
- Always handle loading, error, and empty states for API screens.
- Structure projects with src/screens, components, services, and navigation folders.
Frequently Asked Questions
- Expo or React Native CLI for beginners?
- Expo is faster to start — no Xcode/Android Studio required initially. Move to bare CLI or Expo prebuild when you need custom native modules.
- Do I need a Mac for Android development?
- No. Android builds work on Windows and Linux. macOS is only required for iOS Simulator and App Store builds.
- Why is my Text not showing?
- All text must be inside a <Text> component. Raw strings as children of <View> will not render.
- How do I call localhost API from Android emulator?
- Use http://10.0.2.2:PORT instead of localhost. iOS Simulator can use localhost directly.