How React Native works
React Native lets you write a phone app in React and TypeScript, and the phone shows real Android and iOS screens. This lesson shows what happens between your component and the pixels, what Expo adds, how React Native compares with Flutter, fully native apps and web apps, and ends with the first screen of the Mangrove Rider app running on your own phone.
By the end you can
- Explain what React Native renders, and what the JS thread and the UI thread each do
- Describe the New Architecture (JSI, Fabric, TurboModules), Hermes and Metro in plain words
- Choose between Expo Go and a development build, and know what prebuild, config plugins and EAS do
- Compare React Native, Flutter, fully native and a PWA for a real product decision
- Build a job card with core components, StyleSheet and Flexbox, and run it on a phone
The everyday picture
Think of how a new shelf layout reaches a store. A planner at head office decides the layout: rice on shelf two, milk in the second fridge, a festival display by the door. The planner never touches a shelf. Staff in the store read the plan and put real products on the store's own shelves and fridges. Every store has slightly different fixtures, so the same plan looks a little different in each one, but customers pick up real goods.
React Native works the same way. Your React code is the planner: it decides what the screen should contain. The phone's own UI system is the store staff: it creates real Android and iOS views. A web view inside an app is more like a poster of a shelf. It can look right, but it is a picture of a web page. And how the plan travels matters. Old React Native sent plans in batches by courier. The New Architecture gives the planner a direct phone line to the store.
What React Native really puts on the screen
When you write <View> and <Text> in React Native, no HTML is involved. Each of these host components becomes a real platform view: on Android a View becomes an android.view.ViewGroup, and on iOS a UIView. Scrolling, text input, the keyboard and accessibility come from the operating system, which is why a React Native app feels native and why TalkBack and VoiceOver can read it.
Two threads share the work. The JS thread runs your JavaScript: React renders your components, your event handlers run, your fetch calls start. The UI thread (also called the main thread) is the only one allowed to change native views, and it also receives touches. React Native's renderer turns one into the other in three steps, which the React Native docs call render, commit and mount:
- Render, usually on the JS thread: React runs your components and the renderer builds a matching tree of C++ objects, the shadow tree.
- Commit, on a background thread: Yoga calculates the position and size of every node from your Flexbox styles.
- Mount, on the UI thread: the renderer compares the new tree with the one on screen and creates, updates or removes only the native views that changed.
This split explains most performance problems you will meet. If the JS thread is busy for 300 ms, for example parsing a large list of jobs, a tap still reaches the UI thread at once, but the code that answers the tap waits in line. The button looks frozen. On a cheap phone with 2 GB of RAM, slow JavaScript work is the usual cause of a "laggy" app. Fast on a cheap phone covers measuring and fixing it.
Quick check
What does the phone show when a React Native app renders <Text>Job 4521</Text>?
Host components become real Android and iOS views during the mount step, which runs on the UI thread. There is no web view. Painting every pixel yourself is what Flutter does, which is why the third option is tempting.
The New Architecture, Hermes and Metro in plain words
For years, JavaScript and native code talked through the bridge: every call was turned into a JSON message, queued, sent across, and decoded on the other side. Nothing could happen synchronously, and big messages were slow. The New Architecture replaces it with three parts:
- JSI lets JavaScript hold a reference to a C++ object and call its methods directly. No JSON, no queue, and a call can return a value at once.
- Fabric is the renderer from the diagram above. Its core is C++ shared by both platforms, and it can measure and render synchronously when React needs it, which is what lets React's concurrent features, such as transitions, work on a phone.
- TurboModules are native modules (camera, location, secure storage) called through JSI. They load only when your code first uses them, and Codegen generates a typed contract between JavaScript and native code, which removes a common source of crashes.
The New Architecture has been the default since React Native 0.76, and since 0.82 it is the only architecture: the opt-out flags are ignored. Any library you add must support it. Check its documentation or the React Native Directory before you install it.
Hermes is the JavaScript engine that runs your code, and it is the default. In a release build, your JavaScript is compiled to Hermes bytecode at build time, so the phone does not parse source code at startup. The React Native docs report better startup time, lower memory use and a smaller app compared with the older engine, which matters on a 2 GB Android phone.
Metro is the bundler. During development it runs on your laptop, combines your TypeScript files into one JavaScript bundle and serves it to the phone over your Wi-Fi. When you save a file, Fast Refresh swaps the changed component and keeps its state. For a release, the bundle is built once and shipped inside the app.
Quick check
On the New Architecture, the Rider app's code asks a native location module for the last known position. What is different from the old bridge?
JSI gives JavaScript a direct reference to native objects, so calls can be synchronous and skip serialisation. The module is still native code; only the way you reach it changed. No web view is involved anywhere.
Expo: the framework around React Native
React Native gives you the renderer and the core components. A real app also needs a camera, location, notifications, secure storage, icons, splash screens, signed builds and updates. Expo is the framework that supplies them, and the React Native docs recommend starting new apps with a framework like it. Each Expo SDK release is tested with one React Native version. At the time of writing (October 2026), the newest is Expo SDK 58 with React Native 0.88 and React 19.3.
You run your code in one of two "shells", and choosing the right one saves days:
- Expo Go is a ready-made app from Expo. It already contains the native code of the Expo SDK modules, so you scan a QR code and your JavaScript runs in it in seconds. Expo calls it an educational tool for first steps: you cannot add your own native code, and each Expo Go build runs one SDK version.
- A development build is your own app, compiled with the
expo-dev-clientlibrary. It contains exactly the native libraries you chose, has your app's name and icon, and still loads JavaScript from Metro with Fast Refresh. Real apps use one from the first week.
Where does the native project come from? With Continuous Native Generation, you do not keep the android and ios folders. You describe the app in app.json (name, icons, permissions), and npx expo prebuild generates both folders from a template. A config plugin is a function that edits the generated project during prebuild; libraries ship them, so installing a camera library also adds its Android permission and its iOS usage text. New projects list both folders in .gitignore, and upgrading the SDK means regenerating them.
EAS is Expo's cloud service. EAS Build compiles your app on Expo's servers, so you can make an iPhone build without a Mac. EAS Submit uploads it to the stores, and EAS Update sends JavaScript fixes to installed apps. Testing and releasing covers all three.
Quick check
You want to try a new card layout on your phone in the next ten minutes. It uses only View, Text and Pressable. What is fastest?
Core components are already inside Expo Go, so a QR scan is enough. A development build is the right choice once you need native code Expo Go lacks, but it takes a build and an install. Prebuild generates native folders you do not need yet.
app.json and config plugins, and the same setup becomes your store build.React Native, Flutter, fully native or a PWA?
Four options can put the Rider app on a phone. Each one wins somewhere, and a team should choose for reasons it can say out loud.
| Option | Who draws the UI | Wins when | Costs |
|---|---|---|---|
| React Native (Expo) | The platform's own views | Your team knows React and TypeScript, you want iOS and Android from one codebase, and you want native look, gestures and accessibility | A bridge to native code for anything Expo and libraries do not cover; two platforms to test |
| Flutter | Flutter's own engine paints every pixel | You want the same custom design on every device and the team writes Dart | A separate language and widget set from your web stack; native look is imitated, not inherited |
| Fully native (Kotlin, Swift) | The platform, directly | Deep OS work (widgets, watch apps, heavy camera or audio processing), day-one access to new OS features, or the tightest performance | Two codebases, two teams or two skill sets, every feature built twice |
| PWA (web app) | The browser | Users open it once from a link, no install, instant updates, one web team | Limited background work and device access; on iOS, web push needs the user to add the app to the Home Screen first (iOS 16.4 and later) |
Three questions settle most decisions. Who will build and maintain it, and what do they already know? What device features must work, and do they need to run in the background? How do people arrive: do they install an app they use every day, or tap a link once? A delivery app like Uber or Grab, which riders use all shift, needs background location, reliable notifications and offline storage, so it is an installed app. The page a customer opens from a "your order is on the way" text message is a web page.
Core components, styles and Flexbox on a phone
Six components build most screens. View is the box, like a div. Text holds every piece of text; a string directly inside a View is an error. Image shows local or remote images, and a remote image needs a width and height. Pressable makes anything tappable and tells you when it is pressed. ScrollView scrolls its content; it renders every child at once, so it suits a short screen, and long lists use FlatList instead (see Fast on a cheap phone). TextInput is the text field, with props such as keyboardType="number-pad" for a one-time code.
Styles are JavaScript objects with camelCase names, usually grouped with StyleSheet.create. You pass them in the style prop, and an array merges them, with the last one winning: style={[styles.status, isLate && styles.late]}. Four web habits break:
- No cascade. There are no selectors or class names. A colour set on a
Viewdoes not reach theTextinside it. Text styles are inherited only from a parentText. To share a font, you make your own component, for exampleAppText. - Different Flexbox defaults. Every
Viewis a flex container.flexDirectiondefaults tocolumn, notrow;flexShrinkdefaults to0, not1;alignContentdefaults toflex-start; andflextakes one number, such asflex: 1. A long store name next to a status badge pushes the badge off the screen until you give the nameflexShrink: 1. - Numbers, not units.
padding: 16means 16 density-independent pixels. There is nopx,remorvh. Percent strings such aswidth: '50%'work. - No grid or float. Layout is Flexbox, plus
position: 'absolute'for overlays.
Platforms differ in small ways you must handle. Platform.OS is 'android' or 'ios', and Platform.select picks a value per platform, for example a shadow on iOS and elevation on Android. Phones also have notches, rounded corners and gesture bars, so content must stay inside the safe area. Use SafeAreaView or useSafeAreaInsets from react-native-safe-area-context, which Expo's default template already includes. The SafeAreaView built into React Native is deprecated.
Quick check
A job card shows the store name and a "Late" badge in a row. With "Mangrove Market, Riverside Road North", the badge disappears off the right edge. What fixes it?
In React Native, flexShrink defaults to 0, so the long text refuses to shrink and pushes the badge out. On the web the default is 1, which is why the same layout worked there. React Native has no grid and no rem units.
Build it
Create an Expo app, run it on your own phone, and build the Rider app's first screen: a list of job cards. You need Node.js (an LTS version) and a phone on the same Wi-Fi as your laptop. Commands work in Terminal on macOS and in PowerShell or Git Bash on Windows.
-
Create the project and start Metro
Terminalnpx create-expo-app@latest rider-app cd rider-app npx expo startThe default template uses TypeScript and Expo Router, with screens in an
appfolder (recent templates put it undersrc).npx expo startstarts Metro and prints a QR code. Versions move fast here: at the time of writing, during the move to SDK 58, the plain command creates an SDK 57 project, which Expo's docs recommend if you will use Expo Go on a phone. Adding--template default@sdk-58creates an SDK 58 project. -
Open it on a real phone
The easiest phone for this is an Android one, which is also what most riders carry. Install Expo Go from the Google Play Store, then scan the QR code from inside Expo Go. iPhones are harder in 2026: the App Store copy of Expo Go stops at SDK 54, so for newer SDKs Expo's setup guide builds your own copy with
npx eas-cli@latest goand installs it through TestFlight, which needs a paid Apple Developer Program membership. On a Mac, the iOS Simulator is a free alternative. On iOS, sign in to the same Expo account in Expo Go and withnpx expo login. If the phone cannot connect, for example on office or public Wi-Fi, restart with a tunnel, which is slower but works across networks:Terminalnpx expo start --tunnelChange the title text in the home screen file and save. The phone updates in about a second. That is Fast Refresh.
-
Add the job type and sample data
Create a
features/jobsfolder next to theappfolder, with the job type from the Rider API. Money is integer cents, so$12.50never turns into12.499999.TypeScript// features/jobs/types.ts export type JobStatus = 'assigned' | 'accepted' | 'picked-up' | 'delivered' | 'failed'; export type Job = { id: string; orderId: string; storeName: string; dropAddress: string; dropLat: number; dropLng: number; status: JobStatus; codCents: number; // 0 means prepaid slot: { start: string; end: string }; // ISO times, for example a 30-minute "Now" slot }; export type JobCardView = { title: string; statusLabel: string; codLabel: string; late: boolean; };Add
features/jobs/sampleJobs.tsexportingsampleJobs: Job[]with three jobs: one prepaid, one cash on delivery of 1250 cents, and one whose slot ended an hour ago. -
Write the logic as a plain function
Keep what the card shows out of the component, so you can test it without a phone. Solve the first Practice task below, then put your function in
features/jobs/jobCardView.tswith types:export function jobCardView(job: Job, nowIso: string): JobCardView. -
Build the card with core components
TypeScript// features/jobs/JobCard.tsx import { Platform, Pressable, StyleSheet, Text, View } from 'react-native'; import type { Job } from './types'; import { jobCardView } from './jobCardView'; type Props = { job: Job; now: string; onPress: (id: string) => void }; export function JobCard({ job, now, onPress }: Props) { const view = jobCardView(job, now); return ( <Pressable accessibilityRole="button" onPress={() => onPress(job.id)} style={({ pressed }) => [styles.card, pressed && styles.pressed]} > <View style={styles.row}> <Text style={styles.store} numberOfLines={1}>{view.title}</Text> <Text style={[styles.status, view.late && styles.late]}> {view.late ? 'Late' : view.statusLabel} </Text> </View> <Text style={styles.address} numberOfLines={2}>{job.dropAddress}</Text> <Text style={styles.cod}>{view.codLabel}</Text> </Pressable> ); }TypeScript// the end of JobCard.tsx const styles = StyleSheet.create({ card: { backgroundColor: '#ffffff', borderRadius: 12, padding: 16, marginBottom: 12, ...Platform.select({ ios: { shadowColor: '#000000', shadowOpacity: 0.1, shadowRadius: 6, shadowOffset: { width: 0, height: 2 } }, android: { elevation: 2 }, }), }, pressed: { opacity: 0.7 }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, store: { flexShrink: 1, fontSize: 17, fontWeight: '600' }, // shrink, or the badge is pushed off status: { marginLeft: 8, fontSize: 14, color: '#2f6f4f' }, late: { color: '#b3261e', fontWeight: '700' }, address: { marginTop: 4, fontSize: 15, color: '#444444' }, cod: { marginTop: 8, fontSize: 16, fontWeight: '600' }, });Note the row:
flexDirection: 'row'is needed because the default is a column. EachTextsets its own font, because nothing cascades from theView. -
Show the list on the home screen
Replace the home route (the
index.tsxin theappfolder). If your home route sits one folder deeper, add one more../to the imports.TypeScriptimport { ScrollView, StyleSheet, Text } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { JobCard } from '../features/jobs/JobCard'; import { sampleJobs } from '../features/jobs/sampleJobs'; export default function HomeScreen() { const now = new Date().toISOString(); return ( <SafeAreaView style={styles.screen} edges={['top']}> <Text style={styles.heading}>Jobs for today</Text> <ScrollView contentContainerStyle={styles.list}> {sampleJobs.map((job) => ( <JobCard key={job.id} job={job} now={now} onPress={(id) => console.log('open job', id)} /> ))} </ScrollView> </SafeAreaView> ); } const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#f2f4f3' }, heading: { fontSize: 22, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 12 }, list: { paddingHorizontal: 16, paddingBottom: 24 }, });On the phone, check three things: the heading clears the notch or status bar, the late job shows a red "Late", and a long store name ends with "…" instead of pushing the badge away. Tap a card and the "open job" line appears in the terminal where Metro runs. Navigation and app structure turns that tap into a job detail screen.
-
Know your next step: a development build
Expo Go is enough for this screen. The day you add a native library Expo Go does not contain, make a development build instead. These commands install the dev client, log in to EAS, create
eas.jsonwith a development profile, and build an Android app you install on your phone:Terminalnpx expo install expo-dev-client npm install -g eas-cli eas login eas build:configure eas build --profile development --platform androidAfter you install it,
npx expo startopens your code in your development build instead of Expo Go. Install native libraries withnpx expo install, which picks the version that matches your SDK.
Bring it to your team
Questions to ask when a team starts a React Native app or adds to one.
- Which Expo SDK and React Native version are we on, and who owns upgrading once or twice a year?
- Do all our native libraries support the New Architecture? Where did we check?
- Are we testing on a cheap Android phone with 2 or 3 GB of RAM, or only on our own phones?
- Are the
androidandiosfolders generated by prebuild, and are our native changes in config plugins instead of hand edits? - Is logic such as status labels, money formatting and late flags in plain functions with tests, outside the components?
Red flags
- "It works in Expo Go" used as proof that a feature that needs a native library is done.
- Hand edits in a generated
androidfolder that the next prebuild will wipe out. - Money stored as dollars in floating point, such as
12.5, instead of integer cents. - A library installed with plain
npm installat a version that does not match the SDK. - Choosing a platform because "everyone uses it", with no answer on team skills, device features and how users arrive.
Practice
What the job card shows
Write jobCardView(job, nowIso). It returns { title, statusLabel, codLabel, late }. title is the store name. statusLabel maps the status: assigned to "New job", accepted to "Accepted", picked-up to "On the way", delivered to "Delivered", failed to "Failed", anything else to "Unknown". codLabel is "Collect $12.50" for 1250 cents and "Prepaid" for 0. late is true only when the time now is after slot.end and the job is not yet delivered or failed.
function jobCardView(job, nowIso) {
// job: { id, storeName, dropAddress, status, codCents, slot: { start, end } }
// times are ISO strings such as '2026-10-20T18:30:00Z'
return {
title: job.storeName,
statusLabel: job.status,
codLabel: 'Collect ' + job.codCents / 100,
late: false,
};
}
const base = { id: 'j1', storeName: 'Mangrove Riverside', dropAddress: '12 Lake Road', status: 'accepted', codCents: 0,
slot: { start: '2026-10-20T18:00:00Z', end: '2026-10-20T18:30:00Z' } };
test('status codes become words a rider reads', () => {
expect(jobCardView({ ...base, status: 'picked-up' }, '2026-10-20T18:10:00Z').statusLabel).toBe('On the way');
expect(jobCardView({ ...base, status: 'assigned' }, '2026-10-20T18:10:00Z').statusLabel).toBe('New job');
expect(jobCardView({ ...base, status: 'returned' }, '2026-10-20T18:10:00Z').statusLabel).toBe('Unknown');
});
test('cash on delivery shows dollars and two-digit cents from integer cents', () => {
expect(jobCardView({ ...base, codCents: 1250 }, '2026-10-20T18:10:00Z').codLabel).toBe('Collect $12.50');
expect(jobCardView({ ...base, codCents: 905 }, '2026-10-20T18:10:00Z').codLabel).toBe('Collect $9.05');
expect(jobCardView({ ...base, codCents: 7 }, '2026-10-20T18:10:00Z').codLabel).toBe('Collect $0.07');
});
test('a prepaid order says Prepaid', () => {
expect(jobCardView(base, '2026-10-20T18:10:00Z').codLabel).toBe('Prepaid');
});
test('late only after the slot ends, and only while the job is still open', () => {
expect(jobCardView(base, '2026-10-20T18:31:00Z').late).toBe(true);
expect(jobCardView(base, '2026-10-20T18:29:00Z').late).toBe(false);
expect(jobCardView({ ...base, status: 'delivered' }, '2026-10-20T18:45:00Z').late).toBe(false);
expect(jobCardView({ ...base, status: 'failed' }, '2026-10-20T18:45:00Z').late).toBe(false);
});
Show a solution
const STATUS_LABELS = {
assigned: 'New job', accepted: 'Accepted', 'picked-up': 'On the way',
delivered: 'Delivered', failed: 'Failed',
};
function formatCents(cents) {
const dollars = Math.floor(cents / 100);
const rest = String(cents % 100).padStart(2, '0'); // 905 -> "05"
return '$' + dollars + '.' + rest;
}
function jobCardView(job, nowIso) {
const open = job.status !== 'delivered' && job.status !== 'failed';
return {
title: job.storeName,
statusLabel: STATUS_LABELS[job.status] ?? 'Unknown',
codLabel: job.codCents > 0 ? 'Collect ' + formatCents(job.codCents) : 'Prepaid',
late: open && Date.parse(nowIso) > Date.parse(job.slot.end),
};
}Port a web style and flag what will not work
The back office's job card has web CSS. Write webToNativeStyle(css): it takes an object of CSS properties in kebab-case and returns { style, unsupported }. Convert names to camelCase. Turn values like "12px" into the number 12, and plain number strings like "1" into numbers. Keep other values, such as colours and percentages, as strings. Do not copy, and list in unsupported (in input order), any property that is float or grid-template-columns, any display other than flex or none, any value ending in em, rem, vh or vw, and any flex value that is not a single number.
function webToNativeStyle(css) {
const style = {};
const unsupported = [];
for (const [name, value] of Object.entries(css)) {
const key = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
style[key] = value; // TODO: numbers, and flag what React Native cannot do
}
return { style, unsupported };
}
test('names become camelCase and px values become plain numbers', () => {
expect(webToNativeStyle({ 'background-color': '#ffffff', 'padding-top': '12px', 'border-radius': '8px' }))
.toEqual({ style: { backgroundColor: '#ffffff', paddingTop: 12, borderRadius: 8 }, unsupported: [] });
});
test('grid and float are flagged, not copied', () => {
expect(webToNativeStyle({ display: 'grid', float: 'left', 'margin-left': '4px' }))
.toEqual({ style: { marginLeft: 4 }, unsupported: ['display', 'float'] });
});
test('rem and vh units are flagged, because React Native has no units', () => {
expect(webToNativeStyle({ 'font-size': '1.5rem', height: '100vh' }))
.toEqual({ style: {}, unsupported: ['font-size', 'height'] });
});
test('flex takes a single number only', () => {
expect(webToNativeStyle({ flex: '1' })).toEqual({ style: { flex: 1 }, unsupported: [] });
expect(webToNativeStyle({ flex: '1 1 auto' })).toEqual({ style: {}, unsupported: ['flex'] });
});
test('display flex and percentages are kept as they are', () => {
expect(webToNativeStyle({ display: 'flex', width: '50%' }))
.toEqual({ style: { display: 'flex', width: '50%' }, unsupported: [] });
});
Show a solution
const NOT_IN_RN = new Set(['float', 'grid-template-columns']);
const NUMBER = /^-?\d+(\.\d+)?$/;
const PX = /^-?\d+(\.\d+)?px$/;
function webToNativeStyle(css) {
const style = {};
const unsupported = [];
for (const [name, raw] of Object.entries(css)) {
const value = String(raw).trim();
const bad =
NOT_IN_RN.has(name) ||
(name === 'display' && value !== 'flex' && value !== 'none') ||
/(em|vh|vw)$/.test(value) || // also catches rem
(name === 'flex' && !NUMBER.test(value)); // flex: 1, never '1 1 auto'
if (bad) { unsupported.push(name); continue; }
const key = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (PX.test(value)) style[key] = parseFloat(value);
else if (NUMBER.test(value)) style[key] = Number(value);
else style[key] = value;
}
return { style, unsupported };
}Check yourself
On a cheap Android phone, riders say the "Accept" button takes almost a second to react while the job list refreshes. The refresh parses a large JSON response and re-renders 200 cards. What is the most likely cause?
The UI thread receives the tap at once, but your onPress handler runs on the JS thread, which is busy. Fixes reduce JavaScript work during the refresh, for example fewer re-renders and a virtualised list. The New Architecture has no JSON bridge, and Hermes is not the bottleneck here.
The team wants a third-party barcode library with native Android and iOS code for the pickup scan. What should change in how they run the Rider app?
Expo Go contains only its own built-in native code, so new native code needs your own development build. Config plugins keep the native changes repeatable, while hand-edited generated folders are lost or drift at the next prebuild. Expo supports native libraries well; that is what development builds are for.
Mangrove wants customers to see where their rider is after a "your order is on the way" text message. Most customers will look once or twice per order. What is the best fit?
People arrive from a link and use it briefly, which is where the web wins: no install, instant updates. The installed-app options add a store download for a one-minute task. The Rider app is different, because riders use it all shift and need background features.
Flashcards
Key takeaways
- React Native renders real native views. Your code runs on the JS thread in Hermes; only the UI thread changes views, so a busy JS thread makes taps feel slow.
- The New Architecture (default since 0.76, the only one since 0.82) uses JSI for direct calls, Fabric to render and lazily loaded TurboModules for native features.
- Expo is the framework: SDK modules, Expo Go for quick tries, development builds for real apps, prebuild and config plugins instead of hand-edited native folders, and EAS for builds and updates.
- Choose React Native, Flutter, fully native or a PWA by team skills, device features and how people arrive, not by fashion.
- Styles are objects without a cascade, Flexbox defaults to a column with no shrinking, sizes are unitless, and content stays inside the safe area.
Sources
- About the New Architecture and Render, Commit, and Mount, React Native docs
- The New Architecture is here (0.76), 2024, and React Native 0.82: A New Era, 2025, React Native blog
- Using Hermes, Layout with Flexbox and Text, React Native docs
- Introduction to development builds and Continuous Native Generation, Expo docs
- Safe areas, Expo docs