Pustakam Library

Free Programming learning guide

Learn React Native for App Development

Learn React Native for App Development — a free intermediate-level guide covering how to learn react native for app development. Learn with clear...

94 min read10 chaptersintermediate

What you will learn

  1. Prerequisites & Development Environment
  2. Core Components & Styling
  3. Navigation Patterns
  4. State Management & Data Flow
  5. Device APIs & Native Features
  6. Networking & Data Persistence
  7. Animations & Gestures
  8. Performance & Optimization
  9. Testing & Debugging
  10. Building & Deployment

1. Prerequisites & Development Environment

The Anatomy of a React Native App Imagine you are tasked with building a cross-platform app for a logistics company. The warehouse managers need an iPad app to track inventory, while the drivers need an Android phone app for route optimization. Historically, this meant maintaining two separate codebases: one in Swift for iOS and one in Kotlin for Android. When the product manager requested a change to the package scanning logic, you had to implement it twice, test it twice, and deploy it twice. React Native collapses this duplication. By writing UI components in JavaScript (or TypeScript), you can compile a single codebase into fully native applications for both platforms. But React Native is not a "web wrapper." It does not render your code inside a mobile webview. Instead, it acts as a translator, bridging your JavaScript logic to the native UI primitives of the host platform. To understand how to configure your development environment, you first need to understand the engine you are building upon. The Bridge Architecture and Rendering Pipeline At its core, a React Native application runs across three distinct threads: 1. The JavaScript Thread: This is where your React code lives. It handles state management, component logic, and business rules. When a component's state changes, React calculates the new virtual DOM tree. 2. The Native Thread: This is the main thread of the host OS (iOS or Android). It is responsible for rendering native UI elements (like UIView on iOS or ViewGroup on Android) and handling user gestures. 3. The Shadow Thread: Before the Native Thread can render anything, the layout must be calculated. The Shadow Thread uses a layout engine (traditionally Yoga, a cross-platform layout engine) to compute the exact dimensions and positions of your UI elements based on Flexbox rules. The Bridge is the asynchronous messenger between the JavaScript Thread and the Native Thread. When your JavaScript code renders a <View, the JS thread serializes a JSON message describing the UI hierarchy and passes it across the bridge. The Shadow Thread calculates the layout, and the Native Thread instantiates the actual native view. Because the bridge is asynchronous, the JS thread and Native thread do not block each other. A user can scroll a native list smoothly on the Native Thread while the JS thread is busy processing a large data payload in the background. However, this architecture introduces a bottleneck. If you need to rapidly update the UI—such as dragging an element across the screen or running a complex animation—sending messages back and forth across the asynchronous bridge can cause dropped frames and UI stutter. The New Architecture: Modern React Native (0.68+) introduces the New Architecture, which replaces the asynchronous bridge with JSI …

2. Core Components & Styling

The Anatomy of a React Native UI When you write a React component for the web, the output is eventually serialized into DOM nodes like <div, <span, or <img. The browser's rendering engine takes these nodes, applies CSS, calculates the layout, and paints pixels to the screen. React Native operates on a fundamentally different abstraction. When you write a <View in your JavaScript Thread, the React Native renderer translates that into a native UIView on iOS or a android.view.ViewGroup on Android. This distinction is crucial: React Native does not run a browser engine. There is no DOM. The components you import from react-native are thin JavaScript wrappers that send asynchronous instructions—historically across The Bridge, and now synchronously via JSI (JavaScript Interface)—to the Native Thread to instantiate and manipulate actual native UI elements. Because there is no DOM, there is no standard CSS. Instead, React Native provides a strict subset of CSS implemented via Yoga, a C++ layout engine that calculates Flexbox layouts. To build production-grade applications, you must internalize how these core components map to native equivalents and how the Yoga layout engine interprets your styles. Core Components in Practice React Native offers a handful of foundational components. As an intermediate developer, your focus shifts from simply rendering these components to understanding their specific constraints, performance profiles, and appropriate use cases. View and Text: The Fundamental Building Blocks Unlike the web, where any element can hold text, React Native strictly enforces a separation of concerns: text must be wrapped in a <Text component. A <View attempting to render raw text will throw a native crash. A <View maps directly to a native container. It does not support inheriting styles down the tree, nor does it support pseudo-classes like :hover. Its primary purpose is to establish layout contexts (Flexbox containers) and apply structural styling like borders, backgrounds, and shadows. Image: Handling Sources and Caching The <Image component handles the display of both local assets and remote URLs. While it behaves similarly to an HTML <img tag, it has specific native nuances. For instance, if you do not explicitly define a width and height for a remote image, React Native cannot infer the dimensions before downloading it, resulting in a collapsed layout. For local assets, React Native scales images based on the device's pixel ratio, automatically picking the correct resolution (e.g., image@2x.png) if you follow standard naming conventions. TextInput: Managing User Input <TextInput is highly configurable but requires careful state management. Because it bridges the gap between native text entry and your JavaScript state, it is heavily reliant on the communication speed between threads. Under the New Architecture (JSI), this communication is synchronous, drastically reducing input lag. However, you must still manage …

3. Navigation Patterns

You launch your newly built app. The login screen looks fantastic, styled perfectly using the Flexbox techniques from the previous chapter. The user taps "Login". The screen blinks, and... nothing happens. Or worse, the app crashes. The reality of mobile development quickly sets in: a single screen is a poster, not an application. To build a functional app, you need to move users between screens, pass data, handle the physical back button, and restrict access based on authentication. In React Native, the undisputed standard for this is the React Navigation library. The React Navigation Ecosystem React Navigation is built entirely in JavaScript and runs on the JavaScript Thread. Rather than rendering native screens directly, it acts as a smart orchestrator that manages your navigation state and dictates which React components (screens) should be mounted on the Native Thread at any given time. Because we are assuming an intermediate understanding of React Native, we will skip the basic NavigationContainer setup and dive straight into configuring the three core navigators that form the backbone of almost every mobile application: Stack, Tab, and Drawer. Configuring Stack, Tab, and Drawer Navigators Most applications use a combination of these three navigators. Understanding how they nest is critical to building a scalable architecture. 1. The Stack Navigator The Stack Navigator works exactly like a stack of playing cards. When you navigate to a new screen, it is pushed to the top of the stack. When you go back, it is popped off. This is the default pattern for moving through hierarchical content. Note: We are using createNativeStackNavigator which leverages native navigation primitives on iOS and Android, resulting in native-like swipe gestures and animations that run smoothly off the JS thread. 2. The Tab Navigator Tabs are for top-level destinations that users switch between frequently. React Navigation provides bottom tabs, top tabs (material design), and material bottom tabs. 3. The Drawer Navigator Drawers slide in from the side (usually left) and are excellent for secondary navigation, settings, or account links. Nesting Navigators: A Real-World Pattern A common real-world pattern is to nest a Tab Navigator inside a Stack Navigator. This ensures that when a user navigates from a tab to a detail screen, the detail screen covers the entire screen (including hiding the bottom tabs), and the Android back button naturally pops the detail screen. Passing Parameters and Handling Deep Linking Screens rarely exist in isolation. They need context. Passing Parameters When navigating, you can pass a params object. The destination screen can then read these params via the route prop. Best Practice: While you can pass complex objects as parameters, it is highly recommended to pass only IDs or primitive data. Fetch the full data inside …

4. State Management & Data Flow

You are building a shopping cart. A user taps "Add to Cart" on a product list screen, navigates to their cart via your tab navigator, and the cart is empty. The state didn't survive the navigation. You move the cart array up to the root component and pass it down through five layers of props, but now every "Add to Cart" tap re-renders the entire app. This is the state management dilemma in React Native. State dictates what the user sees, but where you store it and how you route it through your component tree determines whether your app is snappy or sluggish. As we build on the rendering foundations from Core Components & Styling and the screen routing from Navigation Patterns, we now need to wire the actual data logic between them. Local State: useState and useReducer For state that lives and dies within a single component, local hooks are your best tool. Because React Native relies on the JavaScript Thread to calculate UI changes and push them through The Bridge (or JSI under the New Architecture), keeping state localized prevents unnecessary re-renders on the Shadow Thread. Moving Beyond useState for Complex Logic You should already be comfortable with useState for simple, independent values like a modal's isVisible boolean or a text input's string. But when a single user action triggers multiple, related state updates, useState quickly becomes error-prone. Consider a multi-step form input. If you need to update the current step, validate the current fields, and track the number of errors, managing three separate useState calls can lead to race conditions. This is where useReducer shines. useReducer allows you to co-locate related state transitions and enforce strict rules about how state can change. By using useReducer, we guarantee that an email update always clears the error, and advancing a step always validates the email. The logic is testable in isolation, entirely separated from the UI rendering. Handling Side Effects with useEffect State changes trigger re-renders, but sometimes you need to perform an action that isn't strictly about calculating the next UI state—like subscribing to a sensor, fetching data, or manipulating the DOM. These are side effects, handled by useEffect. Because React Native components mount and unmount frequently during navigation, useEffect is notorious for causing memory leaks and infinite loops if mishandled. Avoiding the Infinite Loop The most common pitfall is triggering a state update inside a useEffect without a proper dependency array. If setUserData updates a piece of state that the component relies on, the component re-renders, which fires the useEffect again, creating a cycle that blocks the JavaScript Thread. Rules of thumb for useEffect: 1. Always provide a dependency array. If you omit it entirely, the effect …

5. Device APIs & Native Features

The Permission Economy A user downloads your app, taps "Sign Up," and is immediately greeted with three back-to-back system dialogs: "Allow access to Camera?", "Allow access to Location?", and "Allow access to Photos?" The user taps "Don't Allow" on all three, completes the sign-up, and opens the app—only to find it's a social check-in app that relies entirely on those three features. They close the app and never open it again. This scenario plays out daily across app stores. Device capabilities are what make mobile development distinct from web development, but accessing those capabilities requires navigating a complex social and technical contract with the user—and with the operating system. React Native apps don't run in a browser sandbox, but they don't have unrestricted access to hardware either. iOS and Android both enforce a permission system where sensitive APIs require explicit user consent. The contract has three layers: 1. Static declarations — You describe what your app needs (in Info.plist for iOS, AndroidManifest.xml for Android) before the app even runs. 2. Runtime requests — You ask the user for permission at the moment you actually need it, not before. 3. Graceful degradation — When permission is denied (and it will be, sometimes), your app needs to continue functioning or explain clearly why it can't. If you're using Expo, the Expo Managed Workflow handles much of the static declaration layer through the app.json configuration, but runtime requests are still your responsibility. If you're on the React Native CLI (Bare Workflow), you'll edit native files directly. Either way, the JavaScript-side permission logic is identical. The Permission Lifecycle Both platforms follow roughly the same pattern, but expose it differently. A permission at any given moment is in one of these states: - Undetermined — The user hasn't been asked yet. A request will trigger a system dialog. - Granted — The user said yes. You can proceed with the feature. - Denied — The user said no. On Android, you can ask again. On iOS, asking again does nothing—you must direct the user to Settings. - Blocked / Limited — The user denied permanently (Android) or selected "Limited Photos" (iOS 14+). The critical mistake is assuming that calling a "request" function will always show a dialog. If the permission is already denied on iOS, the request returns immediately with denied—no dialog appears. You need to check status first, then decide whether to request or redirect. Requesting Camera and Media Library Access Let's build a concrete example: a profile photo upload feature that lets users take a photo or pick from their library. We'll use expo-image-picker and expo-camera, but the same patterns apply if you're using community libraries like react-native-image-picker or react-native-vision-camera. First, the static …

6. Networking & Data Persistence

You open your laptop on a cross-country flight, pull up the note-taking app you built last week, and stare at a blank screen with a lone spinner. No notes. No cached data. Just an empty void where your thoughts should be. The app works flawlessly on Wi-Fi, but the moment the network drops, it becomes useless brick. In Chapter 5, we explored how to interact with native device features. Now, we need to tackle the lifeblood of almost every meaningful application: moving data over the network and ensuring it survives when that network disappears. A truly native-feeling app doesn't just fetch data; it remembers it, manipulates it offline, and gracefully handles the messy realities of mobile connectivity. Making API Calls: Fetch, Axios, and Resilience React Native ships with the Fetch API polyfilled globally. If you've built web applications, fetch feels immediately familiar. However, raw fetch has two notorious quirks: it does not throw on HTTP errors (like a 404 or 500), and it lacks built-in support for request timeouts or retries. For basic GET requests, fetch is perfectly adequate. But for intermediate-level applications requiring robust error handling, interceptors, and automatic JSON transformation, Axios is often the preferred tool. Let's look at how to build a resilient networking layer using both. Handling Network Errors and Timeouts A common mistake is treating a fetch promise rejection as a catch-all for network failures. A promise only rejects on a network failure or a CORS issue (the latter being rare in React Native). If the server responds with a 500 Internal Server Error, fetch resolves successfully, and you must manually check response.ok. Here is a robust wrapper around fetch that implements a timeout using Promise.race and properly handles HTTP status codes: Implementing Retry Logic Mobile networks are inherently flaky. A user might pass through a tunnel exactly when your API call is made. Implementing a retry mechanism with exponential backoff ensures transient failures don't result in a broken UX. If you are using Axios, you can apply the axios-retry interceptor. If you are using fetch, you can wrap your calls in a custom retry function. Here is how to implement exponential backoff for a fetch request: By routing your data through a resilient networking layer like this, you prevent the JavaScript Thread from getting stuck handling cascading failures, keeping your UI responsive. Caching Data with AsyncStorage Once you successfully fetch data, the next step is persisting it locally. AsyncStorage is an unencrypted, asynchronous, persistent, key-value storage system. It operates entirely on the native side, communicating over the bridge (or via JSI under the New Architecture). AsyncStorage is perfect for caching JSON responses, user preferences, and auth tokens. A Basic Caching Strategy A common pattern …

7. Animations & Gestures

The Anatomy of a 60fps Animation In the modules covering Core Components and State Management, we established that React Native communicates UI instructions from The JavaScript Thread: over The Bridge to The Native Thread:. This architecture works flawlessly for static UI and navigation transitions. However, when you try to animate a component by updating its state 60 times a second, the JavaScript thread becomes a bottleneck. The JS thread must calculate the new position, serialize it, send it across the bridge, and wait for the native thread to re-render. The result is dropped frames and janky animations. With the introduction of the New Architecture: and JSI (JavaScript Interface), the bridge is bypassed, allowing JavaScript to hold direct references to native objects. But even with JSI, running animation logic on the JavaScript thread is inefficient if it can be avoided. To achieve true 60fps (or 120fps on modern devices), animations must run entirely on the native thread. React Native offers two primary paradigms for this: the built-in imperative Animated API, and the modern, declarative react-native-reanimated library. The Built-in Animated API The Animated API is React Native’s built-in solution for imperative animations. It works by defining animated values (like Animated.Value) and connecting them to component styles via Animated.View. Crucially, Animated can run animations on the native thread using the useNativeDriver flag. Imperative Animations with Animated When using Animated, you define the start and end states, and the library interpolates the values in between. To keep animations off the JavaScript thread, you set useNativeDriver: true. This serializes the animation definition and sends it to the native side before it starts, meaning the JS thread can be completely blocked and the animation will still run smoothly. There is a strict limitation: the native driver only works on non-layout properties. You can animate transform (like translateX, scale, rotation), opacity, and backgroundColor. You cannot animate width, height, top, or left natively, as those require recalculating the layout on the Shadow Thread. Declarative Animations with Animated You can also use Animated declaratively via Animated.timing and Animated.spring tied to component lifecycle events, though it is inherently more imperative than modern alternatives. Let’s look at a real-world example: a loading skeleton shimmer effect. While Animated is powerful, managing multiple intersecting animations and gestures using Animated.event and PanResponder quickly becomes convoluted and often forces you back onto the JavaScript thread. Reanimated 3: Declarative Worklets Reanimated 3 is the modern standard for React Native animations. It leverages JSI (JavaScript Interface) to allow JavaScript functions to be executed synchronously on the UI thread. These functions are called worklets. Unlike Animated, which requires you to manually manage values and interpolate them, Reanimated allows you to write standard React code and use useSharedValues—values that …

8. Performance & Optimization

The Anatomy of a Bottleneck You have built a feature-rich application. It handles complex state, fetches data from a remote API, and renders beautiful animations. But as you add more data to your lists and connect more components to your global state, you notice a hiccup. Scrolling stutters. Button presses feel sluggish. Transitions that were once smooth now drop frames. In React Native, performance degradation usually stems from overloading one of the critical threads we discussed earlier: The JavaScript Thread, The Native Thread, or The Shadow Thread. When the JS thread is blocked performing heavy computations or processing excessive render cycles, it cannot send UI updates across The Bridge (or via JSI under the New Architecture) quickly enough. The result is a dropped frame rate, which the human eye perceives as a stutter. Optimizing a React Native app is fundamentally about minimizing the work done on these threads. This means eliminating unnecessary re-renders, virtualizing massive data sets, and reducing the payload size of your JavaScript bundle and network requests. Profiling Apps to Locate Bottlenecks Before you start wrapping components in memoization functions, you need to identify exactly where the bottleneck lives. Guessing leads to premature optimization, which often complicates code without improving performance. React DevTools: The Component Profiler If your app feels sluggish when navigating or updating state, your first stop is the React Profiler, accessible via the React DevTools. To use it, run your app in development mode and open React DevTools. Record an interaction (like typing in an input or navigating to a new screen) and examine the flamegraph. The profiler shows you exactly how long each component took to render and why it re-rendered. Look for the "Why did this render?" panel in the profiler. It will tell you if a component re-rendered because its props changed, its state changed, or because its parent re-rendered. If a parent component re-renders, all of its children will re-render by default unless they are explicitly memoized. Flipper: Native and Network Inspection While React DevTools handles the React component tree, Flipper is your tool for inspecting the native side and network activity. If your JS thread is unblocked but the app still stutters, the issue might be on the Native Thread or the network. Flipper allows you to: Inspect Network Requests: Monitor all HTTP traffic. If a screen fetches a 5MB JSON payload, the time spent parsing that JSON on the JS thread will cause a massive freeze. View Layout Inspector: See how long the Shadow Thread takes to calculate your flexbox layouts. Deeply nested views can cause layout calculation bottlenecks. Crash and Log Monitoring: View native iOS and Android logs simultaneously to catch native performance warnings. Note: If you are …

9. Testing & Debugging

You’ve just wrapped up the complex animations and gesture handlers from the previous chapter. The UI looks flawless on your simulator. You ship it to your QA team, and within ten minutes, a tester reports a hard crash on an Android device when navigating to the profile screen. No error message, no red screen of death—just a silent, instant close. In React Native, the divide between "it works on my machine" and "it crashes in production" is notoriously wide due to the interaction between the JavaScript Thread and the Native Thread. Automated testing and robust debugging tools are the only way to bridge this gap reliably. This chapter transitions from building features to fortifying them, equipping you with the strategies needed to catch bugs before users do, and diagnosing them swiftly when they slip through. Unit and Component Testing with Jest and RNTL Testing in React Native relies on two core pillars: Jest as the test runner and React Native Testing Library (RNTL) as the component rendering and interaction utility. If you are using the Expo Managed Workflow or React Native CLI (Bare Workflow), Jest is configured out of the box. While Jest handles mocking, assertions, and execution, RNTL handles rendering your components in a simulated React Native environment. The philosophy of RNTL is simple: test your components as a user would interact with them, rather than testing internal implementation details. Testing User Interactions Intermediate testing moves beyond checking if a component renders a specific string. It involves simulating user flows, managing asynchronous state updates, and mocking external dependencies. Consider a LoginForm component that uses navigation and state management concepts from earlier chapters: To test this effectively, we want to simulate a user typing an invalid email, pressing the button, and verifying the error message appears. We also need to wait for state updates to flush before making assertions. Mocking Native Modules When testing components that rely on Device APIs & Native Features, you must mock the native bridge. RNTL provides a preset that mocks standard components, but custom native modules require manual mocks. For instance, if a component uses @react-native-async-storage/async-storage (covered in Networking & Data Persistence), you configure Jest to intercept it: By intercepting these calls, you keep your unit tests purely JavaScript-bound, ensuring they run in milliseconds without hitting the The Bridge. End-to-End Testing While unit tests verify isolated components, End-to-End (E2E) tests verify the entire application flow—from launching the app to interacting with native OS prompts. Because E2E tests run on a real simulator or emulator, they catch issues that RNTL cannot, such as native crash propagation or deep linking failures. Maestro vs. Detox Historically, Detox (built by Wix) has been the standard for React Native E2E …

10. Building & Deployment

You’ve spent weeks perfecting your app. You’ve optimized the JavaScript Thread to avoid frame drops, wired up complex navigation, and ensured your data persistence layers are rock solid. Your tests pass. But when you hand the app to a beta tester, they stare at a default Expo icon, watch a blank white screen for three seconds, and get immediately prompted for location access before they even understand what the app does. First impressions are finalized in milliseconds. Building a production-ready React Native app requires crossing the chasm between a development environment and a polished, signed, store-ready binary. This is where we configure the native project metadata, orchestrate cloud builds, navigate app store bureaucracy, and implement over-the-air updates to bypass the store review process for critical bug fixes. Configuring Native Assets and Permissions In the Expo Managed Workflow, you rarely touch native code directly. Instead, you configure native properties using app.json (or app.config.js). When you eject or use the React Native CLI (Bare Workflow), these settings live directly in Info.plist and AndroidManifest.xml. Since we assume you are using Expo Application Services (EAS) for this workflow, we will focus on the centralized configuration approach. App Icons and Splash Screens Your app icon is the face of your application. It needs to look crisp on a 4-inch iPhone SE and a 12.9-inch iPad Pro. Instead of generating dozens of icon sizes manually, use Expo's icon and splash properties in app.json. You only need to provide a single high-resolution source image (at least 1024x1024 pixels for the icon). EAS Build will automatically resize and route these images to the correct native directories during the build process. Pro Tip: For splash screens, resizeMode: "contain" is generally safer than "cover". If your splash image has text or a logo, "cover" will crop the image on devices with different aspect ratios, potentially cutting off your branding. Native Permissions In Device APIs & Native Features, we accessed capabilities like the camera and location. However, Apple and Google require you to declare why you need these permissions, and they strictly reject apps with overly broad or unexplained permission requests. You configure these in app.json under the ios and android keys. Real-World Example: Imagine you are building a photo-sharing app. If you request READEXTERNALSTORAGE but your permission string says "Allow access to storage," Apple will reject it. The string must explicitly state the value to the user: "Allow access to photos to upload profile pictures." Always write these strings with the user's perspective in mind. Building Standalone Binaries with EAS Build During development, you used the Expo Go app or a Custom Dev Client to load your JavaScript bundle. A standalone binary is a compiled .ipa (iOS) or .aab/.apk (Android) …

Continue learning