Skip to content

Latest commit

 

History

History
1225 lines (959 loc) · 42.1 KB

File metadata and controls

1225 lines (959 loc) · 42.1 KB

React Native Basics

Table of Contents

What is React Native?

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',
  },
});

Bridge vs JSI

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 objects

Comparison:

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);
}

Components vs Native Views

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 → TextView

Key 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>
  );
}

Metro Bundler

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:

  1. Entry Point: Starts from index.jsAppRegistry.registerComponent
  2. Dependency Graph: Follows all import/require statements recursively
  3. Transformation: Babel transpiles TypeScript/JSX to plain JavaScript
  4. Bundling: Combines all modules into one (or few) bundle files
  5. 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.bundle

Metro 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

Hermes Engine

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() or new 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"

Fast Refresh vs Full Reload

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

Platform-Specific Code

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.

Platform.OS

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' },
    }),
  },
});

Platform.select

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);

Platform-Specific Files

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' },
});

Expo vs Bare React Native

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 prebuild

When 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

App Lifecycle

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).

Native App State

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

React Component Lifecycle (Hooks)

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>
  );
}

Interview Questions & Answers

Q1: What is React Native and how does it differ from React?

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

Q2: Explain the Bridge vs JSI. Why is JSI important?

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:

  1. Asynchronous only — JS cannot synchronously read native layout dimensions
  2. JSON serialization overhead — Large payloads are slow
  3. Batching delays — Messages queue up, causing jank
  4. Startup cost — All native modules loaded at launch
  5. 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 bindings

Migration 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).

Q3: What is the difference between React components and native views?

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:

  1. Render Phase: React calls your component functions, building a React element tree
  2. Reconciliation: React diffs the new tree against the previous one
  3. Commit Phase: React Native translates changes into native view operations (create, update, delete)
  4. Native Layout: The native platform calculates sizes and positions
  5. 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> with flex: 1 is the primary layout container (no <div> equivalent)
  • Custom native components can be created with TurboModules and Fabric components

Q4: What is Metro and how does it work?

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:

  1. Resolution: Starting from the entry file (index.js), Metro follows the dependency graph via import/require
  2. Transformation: Babel converts TypeScript/JSX/ESNext to JavaScript the engine understands
  3. Serialization: Modules are wrapped in Metro's module format with unique IDs
  4. Bundling: All modules combined into a single bundle (or split for RAM bundles)
  5. 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 assetExts in metro.config.js

Q5: What is Hermes and why should you use it?

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:

  1. Build time: hermesc compiles JS → .hbc bytecode file
  2. App launch: Engine loads bytecode directly (no parsing)
  3. 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.

Q6: Explain Fast Refresh vs Full Reload.

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:

  1. You save a file
  2. Metro re-compiles only the changed module
  3. The updated module is sent to the device
  4. React re-renders affected components
  5. Component state (useState, useRef) is preserved

Full Reload restarts the entire JavaScript context:

  1. All modules re-evaluated from scratch
  2. All React state lost
  3. All side effects re-run
  4. 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

Q7: How do you write platform-specific code?

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.

Q8: Compare Expo vs Bare React Native. When would you choose each?

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 prebuild generates ios/ and android/ folders
  • Best of both worlds for most production apps

Q9: Explain the React Native app lifecycle.

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

Q10: How does React Native render UI on screen?

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 buffer

Performance Implications:

  • Keep the JS thread free — heavy computation blocks all phases
  • Use React.memo, useMemo, useCallback to reduce reconciliation work
  • Prefer FlatList over ScrollView for long lists (virtualization)
  • Avoid inline styles and anonymous functions in render (cause unnecessary diffs)
  • Use the New Architecture (Fabric) for synchronous layout measurements

Navigation

Previous: Introduction

Next: Core Components