Obi Madu's Blog
Back to all articles
MobileSystem DesignInfrastructure

React Native and Expo: The Mental Model That Makes It Click

Understand how React Native and Expo really work, from native builds to OTA updates and development workflows.

React Native and Expo: The Mental Model That Makes It Click

React Native is incredibly easy to misunderstand if you come primarily from a web development background. You write standard React components, you use JavaScript or TypeScript, you run a familiar-feeling development server, and you get incredibly fast refresh capabilities. Because the workflow feels so close to building React on the web, it is extremely tempting to think of React Native as just a web application neatly wrapped in a mobile shell.

That mental model is entirely wrong.

React Native builds genuine native mobile apps. The UI is fundamentally not rendered into a browser DOM. There are no div or span elements, and there is certainly no hidden web page pretending to be an application. Instead, your React code simply describes a native interface, and React Native acts as the engine that turns that description into real iOS and Android components. Expo then builds on top of React Native by smoothing out the historically rough edges of mobile development, providing native configuration, development builds, file-based routing, over-the-air updates, cloud builds, and streamlined access to common device APIs. Once you deeply understand the boundary between the native runtime and the JavaScript bundle, the entire ecosystem becomes significantly less confusing.

React Native Is Not React DOM

React for the web renders directly to the browser DOM, whereas React Native renders to native UI primitives provided by the operating system.

React webReact Native
divView
span or pText
imgImage
Browser DOMNative iOS and Android views

When you write something like this:

<View style={{ backgroundColor: "red" }}>
  <Text>Hello</Text>
</View>

React Native does not generate HTML under the hood. On iOS, it creates native UIKit views, and on Android, it creates native Android views. That structural distinction matters immensely for performance, styling, accessibility, gestures, native modules, and deployment. You are not shipping a website; you are shipping a compiled native application whose behavior just happens to be driven by a JavaScript engine.

The Two Programs Inside Every React Native App

The most critical concept to grasp in React Native is that your app is really two separate programs working together seamlessly.

Your mobile app

Native runtime <-> JavaScript bundle

The native runtime is the compiled app itself. It is written in platform-native languages and strictly built with native tooling. On iOS, that involves Xcode, Swift, and Objective-C. On Android, that means relying on Gradle, Kotlin, and Java. Conversely, the JavaScript bundle is your actual React application code, which contains your screens, components, navigation hierarchy, state management, and overarching business logic.

The native runtime houses the JavaScript engine, custom native modules, permissions, application metadata, app icons, splash screens, and the vital bridge that allows JavaScript to converse with native APIs. It is compiled, signed, platform-specific, and crucially, it cannot be changed on a user's device without a formal native update through the app store. The JavaScript bundle is far more flexible. During development, it can be seamlessly loaded directly from your computer over a local network. In production, it is typically bundled securely into the app binary, but with over-the-air updates, it can often be updated on the fly without requiring a full app store review.

The browser analogy often helps here: the native runtime is like the Chrome browser, while the JavaScript bundle is like the website. You can push updates to a website endlessly without requiring the user to update Chrome, but if you need a brand new browser feature that Chrome lacks, the browser itself must inevitably be updated.

What Actually Requires a Native Rebuild

This fundamental split explains one of the most common sources of friction and confusion for newcomers: why some changes update instantly on the screen while others frustratingly require a full native rebuild.

ChangeNative rebuild needed?OTA update possible?
Fix UI copyNoYes
Add a new screenNoYes
Fix business logicNoYes
Add a JavaScript-only libraryNoYes
Add a native libraryYesNo
Change app iconYesNo
Change permissionsYesNo

If the change exclusively affects JavaScript, it can usually be refreshed instantaneously and is generally eligible for an over-the-air (OTA) update. However, if the change affects native device capabilities, system permissions, binary metadata, or introduces new native modules, you absolutely need a new native build. Mastering that single rule explains almost every development workflow decision in the React Native ecosystem.

What Expo Adds

Expo is a comprehensive platform purposefully built on top of React Native. It provides a much more complete development environment out of the box and expertly hides the vast majority of native build complexity until you actually need to interact with it.

In an Expo managed project, you rarely ever see the traditional ios/ and android/ folders. Instead, you gracefully configure native behavior through app.json, app.config.js, intelligent config plugins, and various installed packages. When it is eventually time to build the application, Expo can cleanly generate the necessary native projects for you via a single command:

npx expo prebuild

That powerful command creates the native ios/ and android/ projects entirely based on your high-level configuration and listed dependencies. Expo also provides heavily tested native modules, excellent development tooling, file-based routing through Expo Router, EAS Build for managed cloud builds, EAS Submit for automated app store submission, and EAS Update for deploying over-the-air JavaScript updates. The fundamental point is not that Expo removes native code from existence—it merely manages the complexity of it on your behalf.

Expo Go vs Development Builds

Expo Go is a highly convenient, prebuilt app actively maintained by the Expo team. You install it directly from the app store, run npx expo start on your machine, scan a QR code with your phone, and Expo Go instantly loads your JavaScript bundle. It is phenomenal for learning React Native and prototyping ideas rapidly, as you do not need to build anything natively before seeing your app functioning on a physical device.

The primary limitation is that Expo Go only contains the specific native modules already included by the Expo team. You fundamentally cannot test every possible native dependency, custom app icon behavior, production splash screen mechanics, universal links, or complex remote push notification setups natively within it.

A development build is entirely different. It is your own dedicated app binary, explicitly built for development, with the expo-dev-client library installed. You can easily think of it as your own custom version of Expo Go. It actually includes the specific native modules your app legitimately needs to function in production.

FeatureExpo GoDevelopment build
SetupInstall from app storeBuild your own app
Native librariesLimited to Expo Go's included modulesAny native library you include
App icon and splashNot your final appYour real app configuration
Push notificationsLimitedRealistic testing
Best useLearning and prototypesSerious app development

The practical rule is remarkably simple: start with Expo Go if it provides enough functionality for your current needs, but move to development builds the moment your app demands native behavior or third-party libraries that Expo Go simply cannot provide.

Development and Production Load JavaScript Differently

In a development environment, your physical phone usually loads the JavaScript bundle continuously from Metro, which is the local development server running seamlessly on your computer.

Phone running Expo Go or a dev build
        |
        | requests JavaScript bundle
        v
Metro dev server on your computer

That mechanism is exactly why your phone and your computer typically need to be connected to the exact same Wi-Fi network. It is also the underlying reason why fast refresh works so beautifully—the app is continuously polling code from a live development server.

In production, the architecture fundamentally shifts. The finalized JavaScript bundle is packaged securely inside the app binary itself.

YourApp.apk or YourApp.ipa
  native code
  assets
  main.jsbundle

The production app obviously does not need to connect to your local computer. It can launch flawlessly offline, and the JavaScript is heavily optimized and explicitly bundled for performance. EAS Update introduces a compelling hybrid model. The app initially ships with a bundled JavaScript version securely inside, but upon launch, it can quietly check for newer JavaScript updates and cache them dynamically on the device. This capability is incredibly powerful for shipping bug fixes and UI updates seamlessly, but it carries a hard, unyielding boundary: OTA updates absolutely cannot add new native capabilities or modules.

The Shape of an Expo Project

An Expo managed project is typically structured to cleanly separate pure JavaScript code, overarching native configuration, static assets, and essential build configuration.

your-project/
  app/              screens and routes
  components/       reusable UI
  hooks/            custom hooks
  constants/        shared values
  assets/           images and fonts
  app.json          native app configuration
  package.json      dependencies
  eas.json          EAS build and update config

The app.json file comprehensively controls crucial native app metadata.

{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "icon": "./assets/icon.png",
    "android": {
      "package": "com.company.myapp"
    },
    "ios": {
      "bundleIdentifier": "com.company.myapp"
    }
  }
}

The package.json file is critically important because the dependencies listed within it can frequently include native modules. Installing a purely JavaScript package and installing a package bundled with native code are fundamentally different actions with vastly different implications for your build pipeline.

React Native Is Still Frontend Code

While React Native can make a mobile app feel incredibly native, it emphatically does not magically turn frontend code into backend code. A React Native app should absolutely never connect directly to a production database. It should not contain raw database credentials, and it should never store private API secrets within its bundle. APK and IPA files can be trivially inspected by anyone, and JavaScript bundles can easily be extracted by malicious actors.

The correct, secure architecture remains the exact same as most modern frontend systems:

Mobile app <-> Backend API <-> Database

The mobile app should communicate strictly with your API over secure HTTPS. Your API then safely handles complex authentication, robust authorization, thorough validation, strict rate limiting, and private database access. The database remains entirely private and hidden from the mobile client. Exposing an API URL in the client code is perfectly normal, as users can already easily intercept and view network traffic from their own devices. Genuine security does not come from hiding the API URL; it comes entirely from proper authentication, authorization, and rigid server-side enforcement.

Backend-as-a-service tools such as Supabase and Firebase continue to adhere strictly to this pattern. The client securely talks to an intermediary API layer, never directly utilizing raw database credentials with unlimited access. For example, Supabase's public anon key is explicitly designed to be safely exposed to the client, but it must inevitably be paired with proper authentication and strict row-level security on the database side.

const supabase = createClient(
  "https://xyz.supabase.co",
  "public-anon-key"
);

const { data } = await supabase
  .from("interviews")
  .select("*");

The underlying safety inherently comes from the strict policies actively running behind the API, not from pretending that the client-side key is a closely guarded secret.

The Mental Model to Keep

Ultimately, React Native is simply a native container running a highly capable JavaScript application. Expo acts as the robust platform that makes that container significantly easier to configure, build, seamlessly update, and eventually ship to users. Expo Go is a tremendously useful shared prebuilt container, while a development build acts as your own fully customized container. An OTA update uniquely alters the JavaScript running inside that container, completely separate from the container itself.

Once you clearly see and respect that boundary, the entire ecosystem makes substantially more sense. You instinctively understand why adding a new screen is instantaneous, why adding a native library demands a fresh build, why Expo Go eventually stops being sufficient for complex apps, and why mobile apps invariably still require a secure, dedicated backend. That is the entire React Native and Expo learning curve condensed into a single overarching concept: understand what safely lives in JavaScript, what strictly lives in native code, and exactly what crosses the critical boundary between them.

References