- What is React Native?
- Bridge vs JSI
- Components vs Native Views
- Metro Bundler
- Hermes Engine
- Fast Refresh vs Full Reload
- Platform-Specific Code
- Expo vs Bare React Native
- App Lifecycle
- Interview Questions & Answers
Simple Explanation:
React Native is a framework that lets you build mobile apps for iOS and Android using JavaScript/TypeScript and React. Instead of writing separate Swift/Kotlin codebases, you write one React component tree that renders to native UI elements on each platform.
Real-World Analogy:
Imagine you're a chef who speaks one language (JavaScript), but you have two kitchens — one iOS kitchen and one Android kitchen. React Native is the translator who takes your recipe (React components) and instructs each kitchen to prepare the dish using their native ingredients (UIKit views on iOS, Android Views on Android). The final dish looks and feels native on each platform, but you only wrote the recipe once.
Key Concepts:
- Cross-Platform: Write once, run on iOS and Android (with platform-specific tweaks)
- Component-Based: UI is built from reusable React components
- Native Rendering: Components map to real native views, not WebViews
- JavaScript Thread: Business logic runs on a JS thread; UI updates go through the bridge/JSI
- Fast Development: Fast Refresh shows changes instantly during development
- Large Ecosystem: npm packages, Expo tooling, and community libraries
// Your first React Native component
// Think of View as a <div> and Text as a <p> — but they render as native widgets
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export function WelcomeScreen(): React.JSX.Element {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome to React Native!</Text>
<Text style={styles.subtitle}>
This Text component renders as a native UILabel (iOS) or TextView (Android)
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
title: {
fontSize: 24,
fontWeight: '700',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666666',
textAlign: 'center',
},
});Simple Explanation:
The Bridge is the legacy communication channel between JavaScript and native code. It serializes messages as JSON and sends them asynchronously across threads. JSI (JavaScript Interface) is the modern approach that allows JavaScript to hold direct references to native C++ objects, enabling synchronous calls without serialization overhead.
Real-World Analogy:
- Bridge = Sending letters through postal mail. You write a letter (JSON), mail it, wait for a reply. Slow but reliable.
- JSI = Having a direct phone line. You call and get an immediate answer. Fast and synchronous when needed.
// Legacy Bridge flow (conceptual — you don't write bridge code directly)
// JS Thread: "Hey native, create a View with these props"
// ↓ (JSON serialization)
// Bridge Queue (async)
// ↓
// Native Thread: Creates UIView/ android.view.View
// ↓ (JSON serialization)
// Bridge Queue (async)
// ↓
// JS Thread: Receives confirmation
// JSI flow (New Architecture)
// JS holds a direct reference to native C++ HostObject
// Calls methods synchronously — no JSON, no queue
// Think of it as JavaScript having a direct handle to native objectsComparison:
| Feature | Legacy Bridge | JSI (New Architecture) |
|---|---|---|
| Communication | Async JSON messages | Direct C++ references |
| Performance | Serialization overhead | Near-native speed |
| Synchronous calls | Not possible | Supported |
| Type safety | Runtime errors | Codegen provides types |
| Status | Legacy (still works) | Default in RN 0.73+ |
// With New Architecture enabled, native modules become TurboModules
// They load lazily and communicate via JSI instead of the bridge
// Example: Using a TurboModule (conceptual API)
import { NativeModules } from 'react-native';
// TurboModules are loaded on first access, not at startup
// This improves app launch time significantly
const { SettingsManager } = NativeModules;
async function getDeviceSettings(): Promise<void> {
// With TurboModules, this call goes through JSI — much faster
const settings = await SettingsManager.getConstants();
console.log('Device settings:', settings);
}Simple Explanation:
In React Native, you write React components (JavaScript/TypeScript functions that return JSX). The React Native renderer translates these into native views — actual UIKit or Android View objects on the device.
Real-World Analogy:
Think of React components as architectural blueprints and native views as the actual building. The blueprint (component) describes what you want; the construction crew (React Native renderer) builds the real structure (native view) from local materials (platform UI toolkit).
import React from 'react';
import {
View, // → UIView (iOS) / android.view.View (Android)
Text, // → UILabel (iOS) / TextView (Android)
TextInput, // → UITextField (iOS) / EditText (Android)
Image, // → UIImageView (iOS) / ImageView (Android)
ScrollView, // → UIScrollView (iOS) / ScrollView (Android)
Pressable, // → UIButton-like (iOS) / Touchable (Android)
FlatList, // → UICollectionView/UITableView (iOS) / RecyclerView (Android)
} from 'react-native';
// Component tree you write (JavaScript):
// <View>
// <Text>Hello</Text>
// <Pressable><Text>Click me</Text></Pressable>
// </View>
// Native view tree created (platform-specific):
// iOS: UIView → UILabel + UIButton → UILabel
// Android: ViewGroup → TextView + ViewGroup → TextViewKey Differences from Web React:
| Web React | React Native |
|---|---|
<div>, <span>, <p> |
<View>, <Text> |
| CSS styles | StyleSheet (flexbox-based) |
| DOM events | Native gesture system |
<input> |
<TextInput> |
<img> |
<Image> |
<a> |
<Pressable> / <Linking> |
// All text MUST be wrapped in <Text> — unlike web where text can be anywhere
// This is because native platforms require text to be in dedicated text views
function ProfileCard({ name, bio }: { name: string; bio: string }): React.JSX.Element {
return (
<View style={{ padding: 16, backgroundColor: '#FFFFFF', borderRadius: 8 }}>
{/* ✅ Correct: Text inside Text component */}
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>{name}</Text>
<Text style={{ fontSize: 14, color: '#666' }}>{bio}</Text>
{/* ❌ Wrong: Raw text outside Text component — will crash */}
{/* {name} */}
</View>
);
}Simple Explanation:
Metro is the JavaScript bundler for React Native — it takes all your .js/.ts/.tsx files and dependencies, resolves imports, and packages them into a single bundle that runs on the device. Think of it as webpack, but optimized specifically for React Native.
Real-World Analogy:
Metro is like a factory assembly line. Raw materials (your source files and npm packages) enter one end, get processed (transpiled, bundled, minified), and a finished product (the JS bundle) comes out the other end, ready to ship to the device.
// metro.config.js — Metro configuration
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
transformer: {
// Enable experimental features if needed
unstable_allowRequireContext: true,
},
resolver: {
// Add custom file extensions
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json'],
// Asset extensions for images, fonts, etc.
assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ttf', 'otf'],
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);How Metro Works:
- Entry Point: Starts from
index.js→AppRegistry.registerComponent - Dependency Graph: Follows all
import/requirestatements recursively - Transformation: Babel transpiles TypeScript/JSX to plain JavaScript
- Bundling: Combines all modules into one (or few) bundle files
- Serving: In dev mode, serves bundles on-demand via HTTP; in production, bundles are embedded in the app
# Start Metro bundler (development)
npx react-native start
# Start with cache reset (when things break)
npx react-native start --reset-cache
# Production bundle for iOS
npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/main.jsbundle
# Production bundle for Android
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundleMetro vs Webpack:
| Feature | Metro | Webpack |
|---|---|---|
| Target | React Native | Web |
| Tree shaking | Limited | Full |
| Code splitting | Not supported | Supported |
| HMR/Fast Refresh | Built-in | Via plugins |
| Startup speed | Optimized for RN | General purpose |
Simple Explanation:
Hermes is a JavaScript engine optimized specifically for React Native on mobile devices. Unlike V8 or JavaScriptCore, Hermes pre-compiles JavaScript to bytecode at build time, resulting in faster startup times and lower memory usage.
Real-World Analogy:
Traditional JS engines are like cooking from scratch every time you open the app — chop ingredients, mix, bake. Hermes is like meal prepping on Sunday — the food is pre-cooked (bytecode), so when you're hungry (app launch), you just heat it up. Much faster.
// Hermes is enabled by default in React Native 0.73+
// Verify Hermes is running in your app:
declare global {
// Hermes exposes this global when active
const HermesInternal: object | undefined;
}
export function checkHermesEngine(): boolean {
const isHermes = typeof HermesInternal !== 'undefined';
console.log(`Engine: ${isHermes ? 'Hermes ✅' : 'JavaScriptCore'}`);
return isHermes;
}Hermes Benefits:
| Benefit | Description |
|---|---|
| Faster startup | Bytecode pre-compilation reduces parse time |
| Lower memory | Smaller heap footprint than JSC/V8 |
| Smaller app size | Bytecode is more compact than source JS |
| Consistent performance | Same engine on iOS and Android |
| Intl support | Built-in internationalization APIs |
Hermes Limitations:
- No
eval()ornew Function()(security feature) - Some newer JS features may lag behind V8
- Debugging requires Chrome DevTools Protocol support
// Hermes-friendly code patterns
// ✅ Use const/let, arrow functions, template literals
const greet = (name: string): string => `Hello, ${name}!`;
// ✅ Use Map/Set instead of polyfills
const cache = new Map<string, unknown>();
// ❌ Avoid eval and dynamic code generation
// eval('console.log("bad")'); // Not supported on Hermes
// ✅ Use Hermes-compatible Intl for formatting
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
formatter.format(1234.56); // "$1,234.56"Simple Explanation:
Fast Refresh (formerly "Hot Reloading") updates your running app instantly when you save a file, preserving component state. Full Reload restarts the entire JavaScript bundle from scratch, losing all state.
Real-World Analogy:
- Fast Refresh = Replacing a single ingredient in a dish while it's being served — the rest of the meal stays intact.
- Full Reload = Throwing away the entire meal and cooking everything from scratch.
// Fast Refresh works best with function components and hooks
// State is preserved when you edit this component
import React, { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';
export function SearchScreen(): React.JSX.Element {
// This state PERSISTS during Fast Refresh
// Edit the JSX below and save — your typed text stays!
const [query, setQuery] = useState('');
return (
<View style={styles.container}>
<TextInput
style={styles.input}
value={query}
onChangeText={setQuery}
placeholder="Search..."
/>
{/* Try editing this text and saving — Fast Refresh updates instantly */}
<Text style={styles.results}>Results for: {query}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
input: { borderWidth: 1, borderColor: '#CCC', padding: 12, borderRadius: 8 },
results: { marginTop: 16, fontSize: 16 },
});When Fast Refresh Falls Back to Full Reload:
| Scenario | Behavior |
|---|---|
| Edit a component that's not exported | Full reload |
| Edit a file with only non-component exports | Full reload |
| Class component edits | Full reload (prefer function components) |
| Edit module-level side effects | Full reload |
| Syntax error, then fix | Full reload |
Keyboard Shortcuts (Development):
| Action | iOS Simulator | Android Emulator |
|---|---|---|
| Fast Refresh | Automatic on save | Automatic on save |
| Full Reload | Cmd + R | Double R or Cmd + M → Reload |
| Dev Menu | Cmd + D | Cmd + M |
| Toggle Inspector | Cmd + I | — |
Simple Explanation:
iOS and Android have different design guidelines, APIs, and behaviors. React Native provides several ways to write platform-specific code so your app feels native on each platform.
Real-World Analogy:
Platform-specific code is like a restaurant with one menu but different plating for dine-in vs takeout. The food (logic) is the same, but the presentation (UI) adapts to the context.
import { Platform, StyleSheet, View, Text } from 'react-native';
export function PlatformAwareCard(): React.JSX.Element {
return (
<View style={styles.card}>
<Text style={styles.title}>Platform-Aware Card</Text>
<Text>
Running on: {Platform.OS} {Platform.Version}
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,
backgroundColor: '#FFFFFF',
// Platform.select picks the right style object per platform
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
},
title: {
fontSize: 18,
fontWeight: '600',
// Different font weights render differently per platform
...Platform.select({
ios: { fontWeight: '600' },
android: { fontWeight: '700' },
}),
},
});import { Platform } from 'react-native';
// Platform.select works with any value — not just styles
const TAB_BAR_HEIGHT = Platform.select({
ios: 49,
android: 56,
default: 56,
});
const HIT_SLOP = Platform.select({
ios: { top: 10, bottom: 10, left: 10, right: 10 },
android: { top: 8, bottom: 8, left: 8, right: 8 },
});
// Select entire components per platform
const StatusBarComponent = Platform.select({
ios: () => require('./StatusBar.ios').StatusBar,
android: () => require('./StatusBar.android').StatusBar,
})?.() ?? (() => null);components/
├── Header.tsx # Shared logic/types
├── Header.ios.tsx # iOS-specific implementation
└── Header.android.tsx # Android-specific implementation
// Metro automatically picks the right file:
import { Header } from './components/Header';
// On iOS → loads Header.ios.tsx
// On Android → loads Header.android.tsx
// Header.ios.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export function Header({ title }: { title: string }): React.JSX.Element {
return (
<View style={styles.container}>
{/* iOS-style large title header */}
<Text style={styles.largeTitle}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { paddingTop: 60, paddingHorizontal: 16 },
largeTitle: { fontSize: 34, fontWeight: '700' },
});// Header.android.tsx
import React from 'react';
import { View, Text, StyleSheet, StatusBar } from 'react-native';
export function Header({ title }: { title: string }): React.JSX.Element {
return (
<View style={styles.container}>
<StatusBar backgroundColor="#6200EE" barStyle="light-content" />
{/* Material Design app bar */}
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingTop: StatusBar.currentHeight,
backgroundColor: '#6200EE',
height: 56 + (StatusBar.currentHeight ?? 0),
justifyContent: 'center',
paddingHorizontal: 16,
},
title: { fontSize: 20, fontWeight: '500', color: '#FFFFFF' },
});Simple Explanation:
Expo is a set of tools and services built on top of React Native that simplifies development, building, and deploying. Bare React Native (React Native CLI) gives you full control over native code but requires more setup and native development knowledge.
Real-World Analogy:
- Expo = Renting a furnished apartment. Move in quickly, everything works, but you can't knock down walls.
- Bare RN = Building a house from scratch. Total freedom, but you need construction skills (Xcode, Android Studio, Gradle, CocoaPods).
| Feature | Expo | Bare React Native |
|---|---|---|
| Setup time | Minutes | Hours |
| Native code access | Limited (unless prebuild) | Full access |
| OTA updates | Built-in (EAS Update) | Manual (CodePush) |
| Build service | EAS Build (cloud) | Local Xcode/Android Studio |
| Custom native modules | Via config plugins | Direct integration |
| App size | Slightly larger | Optimizable |
| Best for | Startups, MVPs, most apps | Complex native integrations |
// Expo project structure (Expo SDK 50+)
// app.json configures the Expo project
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"version": "1.0.0",
"platforms": ["ios", "android"],
"plugins": [
"expo-router",
"expo-camera"
]
}
}
// Using Expo modules in your app
import { CameraView, useCameraPermissions } from 'expo-camera';
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
export function CameraScreen(): React.JSX.Element {
const [permission, requestPermission] = useCameraPermissions();
if (!permission?.granted) {
return (
<View style={styles.container}>
<Text>Camera permission required</Text>
<Pressable onPress={requestPermission}>
<Text style={styles.button}>Grant Permission</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.container}>
<CameraView style={styles.camera} facing="back" />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
camera: { flex: 1, width: '100%' },
button: { color: '#007AFF', fontSize: 16, marginTop: 12 },
});# Create a new Expo project
npx create-expo-app@latest MyApp --template tabs
# Create a bare React Native project
npx @react-native-community/cli init MyApp
# Expo "prebuild" — generates native projects from Expo config
# Gives you bare-like access while keeping Expo tooling
npx expo prebuildWhen to Choose What:
- Choose Expo if: Starting a new project, team lacks native expertise, need fast iteration, OTA updates are important
- Choose Bare RN if: Heavy native module customization, brownfield integration, strict app size requirements, need full Gradle/Xcode control
- Choose Expo + Prebuild if: You want Expo DX but occasionally need custom native code
Simple Explanation:
React Native apps have lifecycle events at two levels: the native app lifecycle (foreground, background, terminated) and the React component lifecycle (mount, update, unmount via hooks).
Real-World Analogy:
App lifecycle is like a store's operating hours. The store opens (foreground), serves customers (active), might close for lunch (background), and shuts down at night (terminated). React component lifecycle is like individual employees — they clock in (mount), work their shift (update), and clock out (unmount).
import React, { useEffect, useRef, useState } from 'react';
import { AppState, AppStateStatus, View, Text, StyleSheet } from 'react-native';
export function AppLifecycleDemo(): React.JSX.Element {
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState);
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
// Subscribe to app state changes
// Think of this as a security camera watching the store's open/closed sign
const subscription = AppState.addEventListener('change', (nextState) => {
// Detect transitions
if (
appStateRef.current.match(/inactive|background/) &&
nextState === 'active'
) {
// App came to foreground — refresh data, reconnect websockets
console.log('App has come to the foreground!');
refreshData();
}
if (nextState === 'background') {
// App went to background — save state, pause timers
console.log('App went to background');
saveLocalState();
}
appStateRef.current = nextState;
setAppState(nextState);
});
// Cleanup listener on unmount — prevents memory leaks
return () => subscription.remove();
}, []);
return (
<View style={styles.container}>
<Text style={styles.label}>Current App State:</Text>
<Text style={styles.state}>{appState}</Text>
</View>
);
}
function refreshData(): void {
// Re-fetch stale data when app returns to foreground
}
function saveLocalState(): void {
// Persist in-progress work before backgrounding
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
label: { fontSize: 16, color: '#666' },
state: { fontSize: 24, fontWeight: '700', marginTop: 8 },
});App State Values:
| State | Description |
|---|---|
active |
App is in the foreground and receiving events |
inactive |
Transition state (iOS only — e.g., incoming call overlay) |
background |
App is in background but still running |
unknown |
Initial state before first determination |
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
interface User {
id: string;
name: string;
}
export function UserProfile({ userId }: { userId: string }): React.JSX.Element {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// MOUNT: Runs when component first appears on screen
// UPDATE: Re-runs when userId changes
// UNMOUNT: Cleanup function runs when component is removed
useEffect(() => {
let cancelled = false;
async function fetchUser(): Promise<void> {
setLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data: User = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// CLEANUP (UNMOUNT): Cancel in-flight requests
// Prevents "Can't perform state update on unmounted component" warnings
return () => {
cancelled = true;
};
}, [userId]); // Re-fetch when userId prop changes
if (loading) return <ActivityIndicator />;
if (!user) return <Text>User not found</Text>;
return (
<View>
<Text>{user.name}</Text>
</View>
);
}Answer:
React Native is a framework for building native mobile applications using JavaScript/TypeScript and React. React (React DOM) is for building web applications that render to the browser DOM.
Key Differences:
| Aspect | React (Web) | React Native |
|---|---|---|
| Renders to | Browser DOM (HTML) | Native views (UIKit/Android) |
| Styling | CSS | StyleSheet (flexbox subset) |
| Components | div, span, p, input | View, Text, TextInput |
| Navigation | React Router | React Navigation |
| Animations | CSS/Framer Motion | Animated API / Reanimated |
| Platform | Browser only | iOS + Android |
React Native uses the same React paradigm (components, props, state, hooks) but with a different renderer. Instead of creating DOM nodes, the React Native renderer creates native view instances through the bridge or JSI.
// Same React patterns, different primitives
// Web React:
// return <div className="card"><p>{title}</p></div>;
// React Native:
import { View, Text } from 'react-native';
// return <View style={styles.card}><Text>{title}</Text></View>;Why React Native over native development?
- Single codebase for iOS and Android (~90% code sharing)
- Hot/Fast Refresh for rapid iteration
- Large JavaScript ecosystem and developer pool
- Near-native performance for most use cases
- Can drop down to native code when needed
Answer:
The legacy Bridge is an asynchronous communication layer between the JavaScript thread and the native (UI) thread. All data crossing the bridge must be serialized to JSON, sent as a batch of messages, and deserialized on the other side. This creates latency and prevents synchronous native calls.
Problems with the Bridge:
- Asynchronous only — JS cannot synchronously read native layout dimensions
- JSON serialization overhead — Large payloads are slow
- Batching delays — Messages queue up, causing jank
- Startup cost — All native modules loaded at launch
- No type safety — Runtime errors from mismatched types
JSI (JavaScript Interface) solves these by allowing JavaScript to hold direct references to C++ host objects:
// With JSI, a native module method call looks like this internally:
// jsRef.nativeMethod(args) — direct function call, no JSON
// Practical impact you CAN discuss in interviews:
// 1. Synchronous native calls (impossible with bridge)
import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window'); // Synchronous via JSI
// 2. TurboModules — lazy-loaded native modules
// Only loaded when first accessed, not at app startup
// 3. Fabric — new rendering system
// React shadow tree directly creates native views via JSI
// Enables synchronous layout measurements
// 4. Codegen — type-safe native interfaces
// TypeScript specs generate C++ and Java/ObjC bindingsMigration Path: React Native 0.73+ enables New Architecture by default. The bridge still exists for backward compatibility but new development should target JSI-based APIs (TurboModules, Fabric).
Answer:
React components are JavaScript functions (or classes) that describe UI using JSX. They exist only in the JavaScript runtime and are cheap to create and destroy.
Native views are platform-specific UI objects (UIView on iOS, android.view.View on Android) that actually appear on screen. They are expensive to create and manage.
The Rendering Pipeline:
- Render Phase: React calls your component functions, building a React element tree
- Reconciliation: React diffs the new tree against the previous one
- Commit Phase: React Native translates changes into native view operations (create, update, delete)
- Native Layout: The native platform calculates sizes and positions
- Native Paint: Views are drawn on screen
// When this component renders:
function Button({ label, onPress }: { label: string; onPress: () => void }) {
return (
<Pressable onPress={onPress} style={{ padding: 12, backgroundColor: '#007AFF' }}>
<Text style={{ color: '#FFF' }}>{label}</Text>
</Pressable>
);
}
// React Native creates/updates this native hierarchy:
// Pressable → Native touchable wrapper (platform-specific)
// └── Text → UILabel (iOS) or TextView (Android)Important Rules:
- Not all React components map 1:1 to native views (Fragments, context providers don't)
<Text>must wrap all text content (native requirement)<View>withflex: 1is the primary layout container (no<div>equivalent)- Custom native components can be created with TurboModules and Fabric components
Answer:
Metro is the JavaScript bundler built specifically for React Native by Meta. It transforms, bundles, and serves your JavaScript code to the app.
Core Pipeline:
- Resolution: Starting from the entry file (
index.js), Metro follows the dependency graph viaimport/require - Transformation: Babel converts TypeScript/JSX/ESNext to JavaScript the engine understands
- Serialization: Modules are wrapped in Metro's module format with unique IDs
- Bundling: All modules combined into a single bundle (or split for RAM bundles)
- Serving: Development server delivers bundles on-demand; production embeds them in the app
// Metro resolves this import chain:
// index.js → App.tsx → HomeScreen.tsx → api/users.ts → node_modules/axios
// Each file becomes a module in the bundle:
// __d(function(global, require, module, exports) {
// // Your transpiled code here
// }, moduleId, { dependencyMap });Key Configuration Options:
// metro.config.js
module.exports = {
resolver: {
// Resolve .ts/.tsx files
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json'],
// Alias for monorepo packages
extraNodeModules: {
'@shared': path.resolve(__dirname, '../shared'),
},
},
transformer: {
// Inline requires for faster startup
inlineRequires: true,
},
};Development vs Production:
- Dev: Unminified, source maps, Fast Refresh, served over HTTP
- Production: Minified, no source maps, embedded in app binary, tree-shaken (limited)
Common Issues:
- Stale cache: Fix with
npx react-native start --reset-cache - Duplicate modules: Check for symlinked packages in monorepos
- Asset not found: Verify
assetExtsin metro.config.js
Answer:
Hermes is an open-source JavaScript engine optimized for React Native on mobile. Created by Meta, it prioritizes startup time, memory usage, and app size over raw execution speed.
How Hermes Differs from JSC/V8:
| Aspect | JavaScriptCore | Hermes |
|---|---|---|
| Compilation | JIT at runtime | AOT to bytecode at build time |
| Startup | Parse + compile JS | Load pre-compiled bytecode |
| Memory | Higher heap usage | ~30% lower memory |
| App size | Source bundle | Compact bytecode |
| Debugging | Safari/Chrome DevTools | Chrome DevTools Protocol |
Bytecode Compilation Flow:
- Build time:
hermesccompiles JS →.hbcbytecode file - App launch: Engine loads bytecode directly (no parsing)
- Runtime: Bytecode interpreter executes (with JIT optimizations)
// Verify Hermes in your app
export function getEngineInfo(): { engine: string; version?: string } {
if (typeof HermesInternal !== 'undefined') {
return {
engine: 'Hermes',
version: (HermesInternal as { getRuntimeProperties?: () => Record<string, string> })
.getRuntimeProperties?.()?.['OSS Release Version'],
};
}
return { engine: 'JavaScriptCore' };
}
// Hermes-specific optimizations in your code:
// 1. Avoid unnecessary closures in hot paths
// 2. Use Map/Set (native in Hermes, no polyfill needed)
// 3. Prefer const over let for engine optimizations
// 4. Use Intl APIs for formatting (built into Hermes)When NOT to use Hermes:
- If you depend on
eval()or dynamic code generation - If you need bleeding-edge JS features not yet in Hermes
- Legacy apps with Hermes-incompatible native modules (rare in 0.73+)
Hermes is the default engine in React Native 0.73+ and is recommended for all new projects.
Answer:
Fast Refresh is React Native's development feature that injects updated modules into the running app while preserving React state. It replaced the older "Hot Reloading" in React Native 0.61+.
How Fast Refresh Works:
- You save a file
- Metro re-compiles only the changed module
- The updated module is sent to the device
- React re-renders affected components
- Component state (useState, useRef) is preserved
Full Reload restarts the entire JavaScript context:
- All modules re-evaluated from scratch
- All React state lost
- All side effects re-run
- Equivalent to closing and reopening the app (but faster)
// Fast Refresh preserves this state when you edit JSX below
function Counter(): React.JSX.Element {
const [count, setCount] = useState(0); // ← Preserved during Fast Refresh
return (
<View>
{/* Edit this text and save — count stays the same! */}
<Text>Count: {count}</Text>
<Pressable onPress={() => setCount((c) => c + 1)}>
<Text>Increment</Text>
</Pressable>
</View>
);
}Fast Refresh Limitations (falls back to Full Reload):
- Editing files with non-component exports (constants, utility functions used at module level)
- Changing a module that affects modules outside the React tree
- Syntax errors (fixed ones trigger full reload)
- Class components (migrate to function components)
Best Practices for Fast Refresh:
- Export components as named exports from dedicated files
- Keep side effects in useEffect, not at module level
- Separate component files from utility/logic files
- Use function components exclusively
Answer:
React Native provides three main strategies for platform-specific code:
1. Platform.OS and Platform.select (inline) Best for small differences like styles, constants, or minor logic branches.
2. Platform-specific file extensions (.ios.tsx, .android.tsx) Best for entirely different implementations per platform.
3. Native Modules Best for accessing platform APIs not available in RN core.
import { Platform, PlatformOSType } from 'react-native';
// Strategy 1: Inline platform checks
const config = {
apiUrl: __DEV__ ? 'http://localhost:3000' : 'https://api.example.com',
storagePrefix: Platform.OS === 'ios' ? 'ios_' : 'android_',
navigationBarHeight: Platform.select({ ios: 0, android: 48 }) ?? 0,
};
// Strategy 2: Platform-specific types
type PlatformConfig = {
[key in PlatformOSType]?: { headerStyle: 'large' | 'default' };
};
const headerConfig: PlatformConfig = {
ios: { headerStyle: 'large' },
android: { headerStyle: 'default' },
};
function getHeaderStyle(): 'large' | 'default' {
return headerConfig[Platform.OS]?.headerStyle ?? 'default';
}
// Strategy 3: Native module for platform-specific APIs
import { NativeModules, PermissionsAndroid } from 'react-native';
async function requestStoragePermission(): Promise<boolean> {
if (Platform.OS === 'ios') {
// iOS handles permissions via Info.plist — no runtime request needed
return true;
}
// Android requires explicit runtime permission
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}Interview Tip: Mention that overusing Platform.OS checks leads to messy code. Prefer platform-specific files for large differences and shared abstractions for small ones.
Answer:
Expo is a platform built on React Native that provides managed tooling (Expo SDK, EAS Build, EAS Update, Expo Router). Bare React Native uses the React Native CLI directly with full native project access.
Detailed Comparison:
| Criteria | Expo | Bare RN |
|---|---|---|
| Initial setup | npx create-expo-app (2 min) |
npx @react-native-community/cli init (15+ min) |
| Native code | Via config plugins or expo prebuild |
Direct Xcode/Android Studio access |
| Build process | EAS Build (cloud) or local | Local Xcode + Gradle required |
| OTA updates | EAS Update (built-in) | CodePush or custom solution |
| Native modules | Any module with config plugin | Any module, direct linking |
| Team requirements | JS/TS developers | JS/TS + iOS/Android native devs |
| CI/CD | EAS handles builds | Custom pipeline needed |
| App size | +2-5MB (Expo modules) | Minimal baseline |
Decision Framework:
Choose Expo when:
- Starting a new project (default recommendation in 2024+)
- Team is JavaScript-focused
- Need rapid prototyping and OTA updates
- App uses common native features (camera, location, notifications)
Choose Bare RN when:
- Integrating RN into existing native app (brownfield)
- Need obscure native modules without Expo support
- Require fine-grained control over native build configuration
- App size is critically constrained
Choose Expo + Prebuild when:
- Want Expo DX but occasionally need custom native code
npx expo prebuildgenerates ios/ and android/ folders- Best of both worlds for most production apps
Answer:
React Native app lifecycle operates at two levels:
Native App Lifecycle (AppState):
| State | iOS | Android | Typical Actions |
|---|---|---|---|
| Active | Foreground | Foreground | Resume timers, reconnect WS |
| Inactive | Transitioning | N/A | Pause animations |
| Background | Background | Background | Save state, stop location |
| Terminated | Killed | Killed | N/A (cold start next launch) |
React Component Lifecycle (Hooks equivalent):
| Class Lifecycle | Hooks Equivalent | When |
|---|---|---|
| constructor | useState initial value | Component creation |
| componentDidMount | useEffect(() => {}, []) | After first render |
| componentDidUpdate | useEffect(() => {}, [deps]) | After re-render with changed deps |
| componentWillUnmount | useEffect cleanup return | Before removal |
| shouldComponentUpdate | React.memo / useMemo | Prevent re-render |
import React, { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
export function useAppLifecycle(callbacks: {
onForeground?: () => void;
onBackground?: () => void;
}): void {
const appState = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState) => {
if (appState.current.match(/inactive|background/) && nextState === 'active') {
callbacks.onForeground?.();
} else if (nextState === 'background') {
callbacks.onBackground?.();
}
appState.current = nextState;
});
return () => subscription.remove();
}, [callbacks.onForeground, callbacks.onBackground]);
}
// Usage in a screen component
function HomeScreen(): React.JSX.Element {
useAppLifecycle({
onForeground: () => {
// Refresh stale data, restart animations
refetchNotifications();
},
onBackground: () => {
// Flush analytics, save draft content
analytics.flush();
},
});
return (/* ... */);
}Common Lifecycle Pitfalls:
- Not cleaning up AppState listeners (memory leak)
- Not cancelling fetch requests on unmount (state update on unmounted component)
- Assuming background execution time (iOS gives ~30 seconds, then suspends)
- Not handling cold start vs warm start differently
Answer:
React Native rendering follows a multi-phase pipeline from your JSX to pixels on screen:
Phase 1: Render (JavaScript Thread) React calls your component functions, producing a tree of React elements (lightweight JS objects describing UI).
Phase 2: Reconciliation (JavaScript Thread) React diffs the new element tree against the previous one, determining the minimal set of changes needed.
Phase 3: Commit (Bridge/JSI → Native Thread) Changes are sent to the native side as create/update/delete operations on native views. With Fabric (New Architecture), this happens synchronously via JSI.
Phase 4: Layout (Native Thread) Yoga layout engine calculates sizes and positions using flexbox rules. Constraints flow down the tree; sizes flow back up.
Phase 5: Paint (Native Thread) Native views draw themselves on the GPU. UIKit/Core Animation on iOS; Android's RenderThread on Android.
// Simplified rendering flow for this component:
function Card({ title }: { title: string }) {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text style={{ fontSize: 18 }}>{title}</Text>
</View>
);
}
// 1. Render: Creates React elements { type: View, props: {...}, children: [...] }
// 2. Reconcile: Diffs against previous render
// 3. Commit: Creates/updates native View and Text nodes
// 4. Layout: Yoga calculates View = full screen, Text = intrinsic size
// 5. Paint: Native views drawn to screen bufferPerformance Implications:
- Keep the JS thread free — heavy computation blocks all phases
- Use
React.memo,useMemo,useCallbackto reduce reconciliation work - Prefer
FlatListoverScrollViewfor long lists (virtualization) - Avoid inline styles and anonymous functions in render (cause unnecessary diffs)
- Use the New Architecture (Fabric) for synchronous layout measurements
Previous: Introduction
Next: Core Components