Lesson 3 of 26
Project Structure and the Entry Point
How a typical React Native project is laid out, what actually happens at app launch, and where "the app starts here" lives in both Expo and bare projects.
Open an unfamiliar web project and you know roughly where to look:
index.html, a bundler config, and a component tree hanging off some root
render call. A React Native project has an equivalent shape — it just has
one extra layer, because there's a real native app underneath your
JavaScript, not a browser.
The shape of a typical project
A React Native project's exact folder layout depends on whether it's Expo managed or bare CLI, but the recognizable pieces are consistent:
- A JavaScript/TypeScript entry file — commonly
App.js/App.tsxat the project root, or anapp/directory if the project uses a file-based routing convention (as Expo Router–style projects do). This is where your component tree begins. package.json, exactly like any Node-based JS project — dependencies, scripts, and project metadata.- A bundler configuration,
metro.config.js. Metro is React Native's JavaScript bundler — conceptually the same job a web bundler does (resolving imports, packaging source and assets into a bundle), just producing a bundle a native runtime loads instead of one a browser fetches. ios/andandroid/native project folders — present and directly editable in a bare React Native CLI project (these are real Xcode and Android Studio/Gradle projects), and present but generally not hand-edited day-to-day in a typical Expo managed project, since Expo's tooling and EAS Build handle that layer for you.
None of this is exotic once you see it laid out — it's the same "source code plus bundler plus native shell" shape any cross-platform JS runtime needs, just with two real native projects instead of a browser as the final consumer of your bundle.
The entry point: where "the app starts" actually lives
On the web, the browser loads index.html, which loads your bundle, which
typically calls something like createRoot(...).render(<App />). React
Native needs the equivalent handshake, but the "browser" side of that
handshake is native code instead.
Conceptually, the native app shell needs to be told which JavaScript
component is the root of your app. That's what AppRegistry.registerComponent
does at its core — it's the call that tells the native runtime "when you're
ready to show a UI, this is the component to render":
import { AppRegistry } from "react-native";
import App from "./App";
AppRegistry.registerComponent("MyApp", () => App);In an Expo managed project, this registration is handled for you as part of the managed entry point — you generally don't write this call yourself, but the underlying idea is identical: some registration step tells native code which component is the app's root.
The boot sequence, conceptually
Because there's a real native app underneath, launching a React Native app has one more step than a web page load. At a conceptual level — the exact mechanics differ between the old and newer native runtimes, which is a later lesson's topic — the sequence looks like this:
- Step 1
Native app launches
The OS starts the native app shell, same as launching any native iOS or Android app.
- Step 2
JS bundle loads
The native side loads and begins executing the JavaScript bundle produced by Metro.
- Step 3
Root component registers and renders
The registered root component (App) runs — the same React rendering you already know, hooks and all.
- Step 4
UI appears
The rendered component tree is turned into real native views, which the user actually sees and can interact with.
The important takeaway isn't the exact API names, which do shift over time and across native-runtime versions — it's the shape: native code has to be running before your JavaScript can execute, and your JavaScript has to run and register a root component before anything visible can appear. "The app starts here" is genuinely two starting points, native and JS, meeting at a registration call.
Why this matters day to day
You'll rarely touch the entry point once a project is set up — most feature
work happens entirely inside your component tree, same as any React app.
But understanding this sequence explains real, ordinary confusions: why a
native-side crash can happen before your JS ever runs, why changing native
project settings (like ios//android/ files in a bare project) requires
a native rebuild rather than just reloading JS, and why "the JS bundle
failed to load" is a distinct category of failure from "a component threw
an error" — the former means your app never got past step 2 above.
What to remember
- A React Native project has your JS/TS source (App.js/App.tsx or an app/ directory), package.json, a Metro bundler config, and native ios/android project folders.
- ios/ and android/ are real native projects — directly editable in bare CLI, generally managed for you in a typical Expo project.
- AppRegistry.registerComponent (or Expo's managed equivalent) is the handshake that tells native code which component is the app's root.
- Boot order is: native app launches, JS bundle loads, root component renders, then real native UI appears — native always comes first.
- The exact API shape can evolve; the conceptual order — native before JS, JS registration before rendering — is the stable part worth remembering.
Check yourself
4 questions · pass 3/4 to unlock Components and Hooks, the Same as React
1.In a bare React Native CLI project, what are the ios/ and android/ folders?
2.Conceptually, what does
AppRegistry.registerComponent(or its Expo-managed equivalent) do?3.What is the practical role of a bundler configuration like metro.config.js in a React Native project?
4.Which best describes the correct order of a React Native app's boot sequence?
4 left to answer