Pustakam Library

Free Programming learning guide

Building Mobile Apps with React Native

Building Mobile Apps with React Native — a free intermediate-level guide covering how to build a mobile app with react native. Learn with clear...

84 min read10 chaptersintermediate

What you will learn

  1. Environment Setup & React Native Architecture
  2. Core Components & Advanced Styling
  3. Navigation and Routing
  4. State Management & Data Fetching
  5. Device Hardware & Native APIs
  6. Local Storage & Offline Support
  7. Animations and Gestures
  8. Performance Optimization
  9. Testing and Debugging
  10. Building and Deploying to App Stores

1. Environment Setup & React Native Architecture

Imagine pushing a critical bug fix to production, only to discover it works flawlessly on your colleague's iPhone but crashes instantly on an Android emulator. You trace the stack trace back to a native module, but you don't have the local environment configured to debug it. Your IDE throws Java compilation errors you've never seen, Xcode complains about missing toolchains, and Metro is stuck in an endless loop. Before you can build robust mobile applications, you need a predictable, fully armed development environment and a mental model of how React Native actually executes your JavaScript. Without these, you are coding blind. Configuring the Development Environment Setting up a React Native environment requires installing the native toolchains for both iOS and Android. Because React Native compiles down to native code, your machine must be able to build native iOS and Android projects. Prerequisites and System Requirements First, ensure you have Node.js and a package manager installed. While npm ships with Node, the React Native community heavily favors pnpm or Yarn for their monorepo support and performance. You will also need Watchman, a service developed by Meta that watches your file system for changes and drastically improves Metro’s hot-reloading performance. - macOS: Required to build iOS applications. Ensure you are on a recent version of macOS. - Windows/Linux: You can build Android applications perfectly well, but iOS builds are natively restricted to Apple hardware via Xcode. Setting Up Xcode for iOS To build and run iOS applications, you need Xcode, Apple's integrated development environment (IDE). 1. Download Xcode from the Mac App Store. This is a massive download; ensure you have ample disk space (roughly 15-20 GB minimum). 2. Open Xcode and agree to the license terms. This will automatically install the required Command Line Tools. 3. Navigate to Xcode Settings Components and download the latest iOS simulator runtimes. 4. Configure your physical iOS device. Open Xcode, go to Settings Accounts, and add your Apple ID. 5. Connect your iPhone via USB. Open the Devices and Simulators window (Cmd + Shift + 2), select your device, and check Connect via network to allow wireless debugging in the future. Setting Up Android Studio Android development requires Android Studio, which provides the Android SDK, emulator, and platform tools. 1. Download and install Android Studio. During the initial setup, ensure the Android SDK, Android SDK Platform, and Android Virtual Device (AVD) components are selected. 2. Install the necessary SDKs. Open the SDK Manager (Configure SDK Manager in the welcome screen). Select the latest Android API level (e.g., API 34) and apply. 3. Configure the Android NDK (Native Development Kit). React Native uses the NDK to compile C++ code used by the framework. Under the SDK …

2. Core Components & Advanced Styling

The Anatomy of a React Native Screen You have a pixel-perfect Figma mockup in front of you. It features a sticky header, a scrolling feed of user posts, and a grid of action buttons anchored to the bottom of the screen. If you were building this for the web, you’d reach for <div, <p, and CSS overflow-y: scroll. In React Native, those web primitives don't exist. Instead, you must map your visual design to React Native’s Core Components. Because React Native translates your JavaScript into actual native platform widgets, you cannot use arbitrary HTML tags. You are restricted to a specific set of components that have native counterparts on both iOS and Android. Understanding how these components differ—and when to use which—is the first step toward building a fluid, native-feeling application. Choosing the Right Container: View, Text, ScrollView, and FlatList At the most basic level, React Native UIs are constructed using four foundational components. Using the wrong one for the wrong job is the most common cause of UI jank and memory leaks for intermediate developers. - <View: The fundamental building block. It maps directly to UIView on iOS and android.view.View on Android. It supports Flexbox layout, styling, and touch handling, but it does not scroll. Think of it as a <div with overflow: hidden by default. - <Text: A component specifically for displaying text. Unlike the web, where you can put text directly inside a <div, all text in React Native must be wrapped in a <Text component. Furthermore, <Text elements can be nested to inherit styling. - <ScrollView: A generic scrolling container. It renders all of its child components at once. If you have 1,000 items in a ScrollView, React Native will create 1,000 native views in memory before the screen even appears. - <FlatList: The high-performance alternative for rendering lists of data. It lazily renders only the items currently visible on the screen, recycling views as the user scrolls. The ScrollView vs. FlatList Trap A common mistake is using a ScrollView for a long, dynamic list simply because it’s easier to style. Because ScrollView renders everything upfront, large lists will cause massive memory spikes and slow down the JavaScript Thread (introduced in Chapter 1), as it struggles to process and pass layout instructions for hundreds of items through the Bridge or JSI to the native side. Use ScrollView for fixed-length, static content—like a settings page or a form. Use FlatList for dynamic, potentially infinite data—like a feed of posts or a list of search results. Here is a real-world example of a social media feed utilizing the correct components: Notice that the FlatList is wrapped in a <View. If you wrap a FlatList in a ScrollView without …

3. Navigation and Routing

A user taps a push notification for a 50% off flash sale. The app opens, flashes the home screen for a split second, and finally lands on the sale page. By the time the screen settles, the user has already tapped back, assuming the app glitched. Navigation in mobile development isn't just about moving from point A to point B; it is the architectural backbone of your user's journey. In the Core Components & Advanced Styling chapter, we built isolated screens using View, Text, and StyleSheet. Now, we need to wire those screens together. Because React Native renders to native components via the Bridge and Shadow Thread, we cannot rely on web paradigms like the URL bar or window.location. Instead, we use React Navigation, the industry-standard routing library that orchestrates native screen transitions and manages the navigation state in the JavaScript Thread. Installing and Configuring React Navigation React Navigation is modular. You install the core library and then piece together the specific navigators and dependencies your app requires. Assuming you followed the Environment Setup & React Native Architecture chapter, you already have your package manager (pnpm, Yarn, or npm) and your iOS/Android environments configured via Xcode and Android Studio. Install the core navigation library and the native stack navigator: Next, install the underlying native dependencies that handle native screen rendering and gestures: For iOS, you must link these native packages via CocoaPods. If you are developing on macOS for iOS, run: (Note: If you are on Windows/Linux targeting Android, the Android NDK and Android SDK Platform handle the native compilation automatically during the Gradle build process, but ensure your Android Virtual Device (AVD) is up to date.) Structuring Your Navigators Most production applications do not rely on a single navigator. Instead, they compose multiple navigators—typically a Stack, Tab, and Drawer—into a hierarchy. The Stack Navigator A Stack Navigator provides a way for your app to transition between screens where each new screen is placed on top of the previous one, much like a stack of cards. By wrapping our app in NavigationContainer, we establish the root navigation state. The createNativeStackNavigator utilizes native navigation primitives (UINavigationController on iOS and FragmentTransaction on Android), ensuring that screen transitions run entirely on the native thread rather than blocking the JavaScript Thread. Combining Tabs and Drawers In real-world apps, you often need a bottom tab bar for primary destinations and a drawer (hamburger menu) for secondary settings. You achieve this by nesting navigators. Example 1: An E-Commerce App Architecture Consider an e-commerce app. You want a bottom tab for Home, Search, and Cart. However, tapping a product requires pushing a ProductDetails screen that covers the tab bar. - You create a Stack as your root. …

4. State Management & Data Fetching

The Boundary Between State and Data Imagine a user opens your e-commerce app, taps into their cart, and sees it empty for a split second before their saved items suddenly pop in. They tap "Checkout," but because the app re-fetches the cart data from the server, the total price fluctuates before settling. Frustrated, they force-quit the app. This scenario is the classic symptom of poorly managed state and unoptimized data fetching. In React Native, the JavaScript Thread is responsible for handling your component logic, state updates, and network responses. If you treat all state the same way—stuffing server data into a global store like Redux or manually triggering Axios requests in useEffect hooks—you inevitably create redundant network requests, race conditions, and UI flickering. To build a responsive mobile app, you must draw a strict boundary between client state (UI toggles, theme preferences, auth tokens) and server state (user profiles, feed data, cart contents). Client state belongs in a lightweight global store like Zustand, while server state belongs in a dedicated data-fetching library like React Query. Choosing a Global State Manager For intermediate React Native development, the days of writing boilerplate-heavy Redux reducers are largely behind us. Redux Toolkit (RTK) standardizes Redux and removes the boilerplate, making it an excellent choice if you are integrating into an existing Redux ecosystem. However, for new projects, Zustand has become the industry favorite due to its minimal API, lack of providers, and incredibly small footprint. Because we assume basic familiarity with React state, we will focus on Zustand for this chapter's implementation. It requires no <Provider wrapper around your navigation hierarchy, meaning you can jump straight into using it. Setting Up a Zustand Store Let’s build a store to handle a user’s theme preference and authentication status—classic client state. To use this in a component, you simply hook into the specific slice of state you need. This ensures your component only re-renders when isDarkMode changes, not when authToken updates. Fetching and Caching with React Query While Zustand handles our UI state, fetching data from an API still requires robust handling. You could use Axios inside a useEffect, but you would be left manually managing isLoading, isError, and data states for every single component. Worse, every screen mount triggers a new network request, draining mobile data and battery. React Query (via the @tanstack/react-query package) acts as an intelligent server-state cache. It handles the loading, error, and success states automatically, and it caches responses so that navigating back and forth between screens feels instantaneous. To set this up, wrap your application’s root navigator in a QueryClientProvider. Assuming you followed the Navigation and Routing chapter, you will add this just above your navigation container. Combining Axios with …

5. Device Hardware & Native APIs

The Permission Protocol: Asking Before Taking Your app doesn't inherently have access to the user's camera, location, or photos. Both iOS and Android sandbox your application, meaning it operates in a strictly bounded environment. To reach outside that sandbox and interact with device hardware, you must explicitly ask the operating system for permission. The permission workflow follows a strict state machine: 1. Undetermined: The user hasn't been asked yet. You need to trigger a system prompt. 2. Granted: The user said yes. You can access the hardware. 3. Denied: The user said no. You cannot trigger the system prompt again. You must direct the user to their device settings. 4. Blocked/Restricted (OS dependent): Permissions are permanently denied or restricted by device policies (like parental controls). Managing this flow manually is tedious. Fortunately, Expo Modules provide a unified API to handle permissions across both platforms. Even if you are not using the Expo managed workflow, you can install the expo- libraries into a bare React Native project. Requesting Permissions in Practice Let’s look at accessing the camera. To do this, you'll use expo-camera. The modern approach uses a hook to subscribe to the permission status, ensuring your UI reacts instantly if the user changes their mind in device settings while the app is open. Notice how we don't just request permission blindly. If !permission.granted, we render a UI that explains why we need the camera before triggering the requestPermission function. This "pre-permission" pattern drastically improves acceptance rates, as users aren't immediately confronted with a cold system prompt. If a user denies the request, calling requestPermission again will immediately return a denied status without showing a prompt. To handle this, you must link the user to the device's settings app using Linking.openSettings() (introduced via React Native's core Linking API). Capturing Reality: Camera and Photo Library With permissions handled, interacting with the camera and photo library becomes straightforward. In modern React Native development, expo-image-picker and expo-camera are the standard tools for these tasks. Taking vs. Choosing Photos There are two distinct user flows for acquiring images: - Capturing: Opening a live camera view within your app (as seen in the example above) and taking a new photo. - Choosing: Opening the system's native photo library UI to let the user select an existing photo. For the "Choosing" flow, expo-image-picker is highly efficient. It handles the heavy lifting of interfacing with the native iOS UIImagePickerController and Android's photo picker. Handling Image Assets When an image is captured or selected, you receive a local URI (e.g., file:///data/user/0/.../cache/IMG1234.jpg). This URI represents the image on the device's local file system. A common pitfall for intermediate developers is attempting to pass this URI directly to a standard fetch …

6. Local Storage & Offline Support

The Offline-First Paradigm Imagine a field technician using your app to inspect remote cell towers. They open it at the base of a mountain, lose cell service, and attempt to log their findings. If your app simply makes a fetch request to a REST API, that request hangs, the UI freezes, and the data is lost when they force-close the app in frustration. Building robust mobile applications demands a fundamental shift in architecture: assuming the network is an enhancement, not a requirement. While State Management & Data Fetching covered retrieving remote data, we must now tackle how to persist that data locally. By leveraging local storage, we create a caching layer that allows our app to boot instantly with stale data while fresh data loads in the background, and enables full functionality when the device drops connectivity entirely. Key-Value Storage with AsyncStorage For simple persistence needs—like saving user preferences, authentication tokens, or feature flags—AsyncStorage is the standard solution. In the React Native ecosystem, the community-maintained @react-native-async-storage/async-storage package provides an asynchronous, unencrypted, persistent key-value storage system. Because storage operations on native platforms involve crossing the Bridge to the native disk I/O, all AsyncStorage methods are asynchronous and return Promises. Implementing a Type-Safe Settings Hook Let’s build a custom hook to persist user preferences. We'll use JSON serialization to store complex objects under a single key. When to use AsyncStorage: - Storing JWT tokens or session identifiers. - Persisting UI state (e.g., "accepted terms" boolean). - Caching small, non-relational payloads. When to avoid AsyncStorage: - Storing large arrays of objects. AsyncStorage reads the entire string into memory; doing this with megabytes of JSON will cause significant UI jank on the JavaScript Thread. - Complex querying. You cannot filter or sort data without pulling it all into memory first. Structured Local Databases When your app requires offline access to complex, relational, or heavily queryable data—like a list of products, a chat history, or a library of articles—you need a structured local database. SQLite via expo-sqlite or react-native-quick-sqlite SQLite is a lightweight, file-based relational database that runs locally on the device. It allows you to execute standard SQL queries, making it ideal for structured data. If you are using Expo (managed or bare), expo-sqlite provides a seamless API. For bare React Native workflows, react-native-quick-sqlite offers high-performance, synchronous and asynchronous bindings backed by JSI (JavaScript Interface), bypassing the older Bridge entirely for faster execution. Let's look at a practical example: building an offline product catalog using expo-sqlite. WatermelonDB for Reactive Data While SQLite is powerful, managing state synchronization and UI reactivity manually becomes tedious. WatermelonDB is a high-performance, offline-first database built on top of SQLite (or LokiJS for web). It is designed to handle massive …

7. Animations and Gestures

The Anatomy of a Fluid Interaction Think about the last time you used a mobile app that felt truly premium. It wasn’t just the colors or the layout that made it feel polished—it was how it moved. When you swiped a list item to delete it, the row didn’t just vanish; it shrank and faded as it slid off the screen. When you dragged a card, it tilted slightly with the friction of your finger. In React Native, achieving this level of fluidity requires bridging the gap between JavaScript logic and native UI rendering. If you try to animate components by updating React state on every frame, the Bridge (introduced in Chapter 1) becomes a bottleneck. The JavaScript thread simply cannot keep up with 60 or 120 frames per second, resulting in janky, stuttering animations. To solve this, React Native provides specialized APIs that offload animation work directly to the native UI thread. We will start with the built-in Animated API for simple transitions, then move to the React Native Gesture Handler for reliable touch capturing, and finally combine them using React Native Reanimated to build high-performance, gesture-driven interfaces. The Built-in Animated API For basic UI transitions—like fading in a modal, sliding a notification banner into view, or pulsing a button—the built-in Animated API is more than sufficient. It uses a declarative approach: you define an animation plan, and an optimized native driver executes it frame-by-frame without crossing the JavaScript bridge. Values and Animation Types The Animated API revolves around Animated Values. Instead of storing a number in React state, you store it in an Animated.Value. There are three primary ways to drive these values: 1. Animated.timing: Animates a value to a specific endpoint over a set duration, using an easing curve. 2. Animated.spring: Animates a value using physics-based spring dynamics. Springs feel much more natural for UI elements than linear or eased timings. 3. Animated.decay: Starts with an initial velocity and gradually slows down to zero. This is highly useful for flick gestures (like a scrolling view that keeps moving after you let go). Enabling the Native Driver To keep animations smooth, you must pass useNativeDriver: true in your animation configuration. This tells React Native to serialize the animation configuration and send it to the native side once. From that point on, the native UI thread handles the frame updates, entirely bypassing the JavaScript thread. There is a catch: the native driver only supports transform properties (like scale, rotate, translateX) and opacity. It does not support animating width, height, or background color natively. Example: A Fading and Scaling Toast Notification Let’s build a simple toast notification that fades and scales in when triggered, then springs out of view. While …

8. Performance Optimization

Hunting Down Unnecessary Re-renders You have built a feature-rich application. It handles complex state, fetches data efficiently, and features smooth gestures. But as the component tree grows, you notice a distinct stutter when typing in a search bar or selecting an item. In React Native, the JavaScript thread is single-threaded. If it is busy executing render functions, it cannot process touch events or animations, resulting in dropped frames and an unresponsive UI. The most common culprit behind a blocked JavaScript thread is unnecessary re-rendering—components re-executing their render functions when their inputs haven't actually changed. Profiling with React DevTools To fix what you cannot see, you need the React DevTools Profiler. If you haven't already, install it globally via your terminal: Run the command react-devtools to launch the standalone profiler window, and connect your running simulator or emulator. To capture a useful profile: 1. Click the record button in the Profiler tab. 2. Interact with your app (e.g., type a character in a text input, or tap a button). 3. Stop the recording. The profiler presents a flame chart. Each bar represents a component that rendered. The width of the bar corresponds to the time it took to render. The color indicates how much time was spent rendering—gray is fast, yellow is slower, and red indicates a severe bottleneck. If you type a single character into a search input and see the flame chart light up with dozens of unrelated components rendering, you have found your bottleneck. Memoization Strategies Once you identify which components are re-rendering unnecessarily, you can prevent it using memoization. React provides three primary memoization tools: - React.memo: A higher-order component that memoizes a functional component. It only re-renders if its props change. - useMemo: A hook that memoizes a calculated value, preventing it from being recalculated on every render. - useCallback: A hook that memoizes a function definition, ensuring the function reference remains stable across renders. Example: Optimizing a Product Card Imagine an e-commerce app. You have a ProductList component and a ProductCard component. When a user taps a "favorite" icon on a single card, the parent ProductList state updates, causing all ProductCard components to re-render. To fix this, wrap the component in React.memo. However, React.memo performs a shallow comparison of props. If onToggleFavorite is an inline function, its reference changes on every parent render, defeating the memoization. You must pair React.memo with useCallback in the parent. Memoization is not free—it incurs a memory cost to store previous values and a CPU cost to perform comparisons. Apply it strategically to components with expensive render logic or those rendering deep trees of children, rather than slapping React.memo on every component. Optimizing List Rendering Rendering large datasets in React …

9. Testing and Debugging

The Anatomy of a Silent Crash You’ve just wired up a complex animation gesture that fetches data from your offline cache, navigates to a new screen, and updates a device hardware API. It works flawlessly on your iPhone 14 Pro simulator. You ship it to your QA team, and on an older Android device, the app freezes for a split second and vanishes back to the home screen. No red error screen. No JavaScript traceback. Just a silent, instant crash. This scenario is the reality of mobile development. JavaScript errors are only the tip of the iceberg; native crashes, race conditions, and platform-specific quirks require a robust strategy to catch and resolve. Up to this point, we’ve relied on manual observation and React Native’s default error boundaries. Now, it’s time to automate our safety net and peer into the native layers of our application. Component and Utility Testing with Jest and RNTL Automated testing in React Native relies on two primary tools working in tandem: Jest as the test runner, and React Native Testing Library (RNTL) to render and interact with components. Jest is configured by default in new React Native projects. It provides an isolated environment that simulates the React Native runtime without needing an actual emulator. If you need to mock native modules you accessed via NativeModules (as discussed in Device Hardware & Native APIs), Jest’s jest.mock function is where you do it. Testing Utility Functions Utility functions—like formatting dates or validating local storage schemas from Local Storage & Offline Support—are pure logic and perfect for standard Jest tests. Testing Components with RNTL RNTL encourages testing components from the user's perspective. Instead of testing internal component state or methods, you query by what the user sees: text, accessibility roles, and test IDs. Consider a component that fetches data and displays a loading state before rendering a list. We can mock the data fetching logic from State Management & Data Fetching and test the component's visual states. To test this, we don't need a real API. We pass a mocked function as a prop. By querying for testID and verifying the rendered children, we ensure the component behaves correctly regardless of its internal implementation. Debugging JavaScript Issues When automated tests pass but manual interaction reveals a bug, you need to inspect the running JavaScript. React Native provides two primary avenues for this: Chrome Developer Tools and Flipper. Chrome Developer Tools You can debug your JavaScript code remotely in Chrome by enabling remote debugging from the developer menu (Cmd+D on iOS, Cmd+M on Android). This connects the React Native runtime to Chrome's V8 engine, allowing you to open chrome://inspect and use standard DevTools. While great for inspecting network requests, setting …

10. Building and Deploying to App Stores

You’ve spent weeks perfecting your React Native application. You’ve optimized list rendering, wired up deep linking, and ensured your offline state management seamlessly syncs when the network returns. The code is tested, debugged, and ready. But an app on a simulator helps no one. The final hurdle—navigating the labyrinth of native build configurations, code signing, and app store review processes—trips up more developers than any runtime error. This is the bridge between a development project and a published product. Configuring Production App Identity Before generating a build, your app needs a production-ready identity. This involves setting the Bundle Identifier (iOS) and Application ID (Android), configuring app icons, and implementing splash screens. Bundle Identifiers and Application IDs The bundle identifier uniquely identifies your app on both operating systems and app stores. By convention, it uses reverse-DNS format (e.g., com.yourcompany.yourapp). For iOS, open ios/YourApp.xcworkspace in Xcode, select your main app target, and navigate to the Signing & Capabilities tab. Update the Bundle Identifier field. For Android, open android/app/build.gradle. Locate the defaultConfig block and update the applicationId: If you generated your project with a specific template, ensure the applicationId matches the package name in your android/app/src/main/AndroidManifest.xml to avoid build-time namespace collisions. App Icons and Splash Screens Manually generating dozens of icon sizes for different screen densities and OS versions is tedious. The industry standard approach is to use a single high-resolution source image and let a tool generate the required assets. Example 1: Automating Assets with react-native-bootsplash A popular, robust approach for handling both splash screens and app icons is react-native-bootsplash. You provide a single 1024x1024 PNG, and the CLI generates all required iOS and Android sizes. First, install the package: Then, run the generator command, pointing to your high-resolution logo: This single command generates the AppIcon.appiconset for Xcode and the mipmap- directories for Android Studio. For iOS, you still need to open Xcode, select Images.xcassets, and ensure the AppIcon target is set to your newly generated folder. For Android, verify that your AndroidManifest.xml references the correct android:icon and android:roundIcon properties. Versioning Your Application Versioning for mobile apps requires tracking two distinct numbers: a human-readable version string and a machine-readable build number. Both platforms require these to be incremented for every production submission. Understanding versionName vs versionCode Version Name (String): What users see in the App Store or Google Play (e.g., 1.0.0). Follow Semantic Versioning (Major.Minor.Patch). Version Code (Integer): An internal number used by the stores to identify unique builds. It must increment with every upload, even if the version name doesn't change. In a standard React Native project, you can manage both platforms from the root package.json using a tool, or configure them natively. For iOS, open ios/YourApp.xcworkspace in Xcode, go to …

Continue learning