skip to content

Platform-Specific Code

Branching one codebase per operating system with Platform.OS and Platform.select, .ios and .android file suffixes, and APIs that exist on one OS only. Interviewers probe where each branch belongs.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

19

In React Native, how does Metro pick between DatePicker.ios.tsx, DatePicker.android.tsx and DatePicker.native.tsx for an import of './DatePicker'?

level: juniorimportance: must knowfreq 58%

answer

  1. one bundle per platform
  2. import without the suffix
  3. platform suffix, then .native, then plain
  4. missing file fails the bundle
  5. other platform's file never ships

basics

~10 s

Import without the suffix. Bundling for a platform, Metro tries that platform's suffix first (.ios or .android), then .native, then the plain file, so each platform's bundle contains only its matching implementation.

solid answer

~40 s

Metro builds a separate bundle per platform and resolves every import for that platform. For `import DatePicker from './DatePicker'` it tries, for each source extension, `DatePicker.ios.tsx` (or `.android.tsx`), then `DatePicker.native.tsx`, then `DatePicker.tsx`, and takes the first file that exists. `.native` is the shared fallback for both iOS and Android, typically used when a plain file is kept for web. The import must omit the suffix: `'./DatePicker.ios'` pins the iOS file on every platform. If a platform has no match, bundling for that platform fails with a resolution error rather than producing `undefined` at runtime. Only the chosen file, and its own imports, end up in each platform's bundle.

code

bash · 6 lines
bash
src/booking/
  DatePicker.ios.tsx       # inline wheel in a bottom sheet
  DatePicker.android.tsx   # opens a dialog
  DatePicker.types.ts      # shared DatePickerProps
  useAppointmentDate.ts    # shared rules, no platform code
  BookingScreen.tsx        # import DatePicker from './DatePicker'

go deeper

for a junior

Recall that Button.ios.tsx and Button.android.tsx are picked automatically when you import './Button', and that .native covers both mobile platforms.

for a middle

Explain Metro's candidate order per source extension, why importing with the suffix pins one file, and why a missing file fails the bundle.

for a senior

Catch the traps in review: explicit suffix imports, mixed extensions, stale plain files, and tests that only resolve the iOS variant.

for a principal

Standardize where per-platform files live and how they are named, so every platform bundle and test run is built and checked, not just the one developers use daily.

## What the suffixes are React Native lets you write one module several times, once per platform, and choose between them by **file name**. A clinic-booking app might ship `DatePicker.ios.tsx`, which shows an inline wheel in a bottom sheet, and `DatePicker.android.tsx`, which opens a dialog. Screens import neither file directly; they write `import DatePicker from './DatePicker'` and get the right one. The selection is done by **Metro**, React Native's bundler, not at runtime. ## How Metro resolves an import Metro produces one JavaScript bundle per platform, and while building it knows which platform it is building for. For a relative import without an extension it generates candidate file names and takes the **first one that exists**: 1. the exact path as written, in case the import already names a file; 2. then, for each source extension in the configured order (`js`, `jsx`, `json`, `ts`, `tsx` by default in a bare project), it tries three names in turn: - `DatePicker.<platform>.<ext>`, for example `DatePicker.ios.tsx`; - `DatePicker.native.<ext>`; - `DatePicker.<ext>`. | Files present | iOS bundle uses | Android bundle uses | |---|---|---| | `.ios.tsx`, `.android.tsx` | `.ios.tsx` | `.android.tsx` | | `.ios.tsx`, `.native.tsx` | `.ios.tsx` | `.native.tsx` | | `.native.tsx`, `.tsx` | `.native.tsx` | `.native.tsx` | | `.ios.tsx` only | `.ios.tsx` | resolution error | Because each bundle contains only the file that won, the Android date picker's code, and anything only it imports, never ships in the iOS app, and vice versa. ## Why this happens at bundle time Resolution is part of building the dependency graph, before any of your code runs. That has three consequences worth stating in an interview. There is **no runtime cost**: the app never asks which file to load, because the bundle already contains exactly one. There is **no runtime switch**: you cannot choose a platform file dynamically, since the other file is simply not in the bundle. And one development server can serve **both platforms at once**: when an iOS simulator and an Android emulator connect to the same Metro process, each requests a bundle for its own platform and each receives its own date picker. ## The .native fallback `.native` means "any React Native platform". Metro's React Native resolver tries it for both iOS and Android after the platform-specific name. React Native's docs describe the typical use: `Container.native.tsx` for the mobile apps and a plain `Container.tsx` for a web bundler, which does not know the `.native` convention and picks the plain file. When iOS and Android share an implementation, `.native` avoids writing the same file twice. ## Import without the suffix The suffix is Metro's decision, so the import must leave it out: - `import DatePicker from './DatePicker'` resolves per platform, as above. - `import DatePicker from './DatePicker.ios'` asks for a module literally named `DatePicker.ios`. Metro still appends platform and source extensions, finds `DatePicker.ios.tsx` on every platform, and the **Android app ships the iOS picker**. Nothing warns you. ## Failure modes - **A missing platform file** is a **bundling error** for that platform, reported when Metro builds the bundle, not a runtime `undefined`. A team that only runs the iOS simulator discovers it when the Android build is made. - **Mixed extensions.** The outer loop is over source extensions, so in a bare project with the default order a plain `DatePicker.js` is found before `DatePicker.ios.tsx`: the `js` round tries `.ios.js`, `.native.js` and `.js` before the `tsx` round starts. Keep every variant of one module in the same extension. - **A shared file shadowed by a stale one.** Leaving an old `DatePicker.tsx` next to new platform files is harmless on mobile, since platform names win, but confusing for readers and for web. - **Tests.** Jest does not run Metro. React Native's Jest preset resolves platform files with iOS as the default platform, so the Android file is not exercised by a default test run. ## In the clinic-booking app The booking screen imports `./DatePicker` once. The iOS and Android files each implement the same props, `value`, `minimumDate` and `onChange`, taken from a shared `DatePicker.types.ts`. Appointment rules, such as blocking past dates, live in a plain hook both files use. The platform files contain only what truly differs: how the picker looks and opens.

  • In a bare React Native project, why can DatePicker.js win over DatePicker.ios.tsx on iOS?
    Metro loops over source extensions in order, and within each extension tries the platform name, then `.native`, then the plain name. With the default order `js, jsx, json, ts, tsx`, the `js` round finds `DatePicker.js` before the `tsx` round ever tries `DatePicker.ios.tsx`. Keep every variant of a module in one extension.
  • Why does a React Native Jest test of a screen using './DatePicker' only exercise the iOS picker by default?
    Jest resolves modules itself, not through Metro. React Native's Jest preset configures platform-aware resolution with `ios` as the default platform, so `.ios` files win. To test the Android file, run a separate Jest project whose default platform is `android`, or import and test each implementation directly.

saying these in an interview costs you the question

  • You should import './DatePicker.ios' so iOS gets the right file.
  • Both platform files are bundled and one is chosen at runtime.
  • A missing .android file makes the import undefined on Android at runtime.
  • The .native suffix is only for iOS.
  • Metro always prefers a platform file regardless of its extension.
open as a page

In React Native, what do Platform.OS and Platform.select return, and how does Platform.select choose between ios, android, native and default keys?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Platform.OS is the running platform's name, 'ios' or 'android'. Platform.select(spec) returns the value under that platform's key, otherwise under native, otherwise under default, otherwise undefined; the value can be anything, from a number to a style object.

open as a page

In React Native, how does BackHandler decide whether an Android back press is handled by your code or exits the app?

level: middleimportance: must knowfreq 62%

basics

~20 s

BackHandler calls hardwareBackPress listeners newest-first; the first one that returns true consumes the press and older listeners never run. If none returns true, React Native falls back to Android's default back behaviour, which leaves the app.

open as a page

In React Native, what does the Android-only elevation style do, and why does a card styled only with iOS shadow props look flat on Android?

level: middleimportance: must knowfreq 55%

basics

~20 s

elevation sets Android's native view elevation, which draws a system shadow and lifts the view above non-elevated siblings. shadowOffset, shadowOpacity and shadowRadius are iOS-only, so on Android only elevation (tinted by shadowColor on API 28+) or boxShadow produces a shadow.

open as a page

In React Native, why does a view with only shadowColor set show no shadow on iOS, and when would you use boxShadow instead of the shadow props?

level: middleimportance: must knowfreq 55%

basics

~20 s

React Native's shadow props map to the iOS layer shadow, and shadowOpacity defaults to 0, so shadowColor alone draws nothing. boxShadow is the web-style alternative that also renders on Android, supports spread, inset and multiple shadows, and needs the New Architecture.

open as a page

In React Native, what does ToastAndroid.show do, and what happens when the same call runs on an iOS device?

level: juniorimportance: should knowfreq 30%

basics

~20 s

ToastAndroid.show(message, duration) shows a native Android toast for ToastAndroid.SHORT or ToastAndroid.LONG. On iOS the module is a fallback stub: the call logs a 'not supported on this platform' warning and shows nothing, so iOS needs another feedback path.

open as a page

In React Native, how do you show a native iOS action sheet with ActionSheetIOS.showActionSheetWithOptions, and what happens if the same call runs on Android?

level: juniorimportance: should knowfreq 35%

basics

~20 s

ActionSheetIOS.showActionSheetWithOptions takes an options array of button titles plus indices for cancel and destructive buttons, and calls back with the zero-based index tapped. It is iOS-only: on Android the native module is missing and the call throws.

open as a page

In a React Native Pressable, how does the android_ripple prop work, and why might a configured ripple never appear?

level: middleimportance: should knowfreq 28%

basics

~20 s

android_ripple draws Android's native ripple on a Pressable, configured by color, borderless, radius, foreground and alpha. It is enabled only when color, borderless or radius is set, iOS ignores it, and a covering child hides it unless foreground is true.

open as a page

In a TypeScript React Native project with only DatePicker.ios.tsx and DatePicker.android.tsx, why does import './DatePicker' fail type-checking, and how is it fixed?

level: middleimportance: should knowfreq 30%

basics

~20 s

TypeScript resolves modules itself and does not know Metro's platform suffixes, so no DatePicker file exists for it. Add a DatePicker.d.ts declaring the shared API, or set moduleSuffixes such as ['.ios', '.native', ''] per platform tsconfig.

open as a page

In React Native, what does Alert.prompt do on iOS, and why does the same text-input prompt never appear on Android?

level: middleimportance: should knowfreq 25%

basics

~20 s

Alert.prompt shows an iOS alert with a text field and passes the text to a callback or to the pressed button's onPress. Its body only runs when Platform.OS is ios, so on Android it returns silently: no dialog, no warning.

open as a page

In React Native, how does DynamicColorIOS pick a colour for light and dark mode, and why can Platform.select({ ios: DynamicColorIOS(...) }) crash on Android?

level: middleimportance: should knowfreq 22%

basics

~20 s

DynamicColorIOS({ light, dark }) returns one colour that iOS resolves natively for the current appearance, with optional high-contrast variants. On other platforms the function throws, and Platform.select's object literal evaluates every branch first, so the iOS branch still runs on Android.

open as a page

In React Native, why is Platform.Version a number on Android but a string on iOS, and how do you compare it safely?

level: middleimportance: should knowfreq 42%

basics

~20 s

On Android Platform.Version is the API level, a number such as 34; on iOS it is the system version string such as '18.2'. Compare Android numerically, parse the iOS major version with parseInt, and check Platform.OS first.

open as a page

After a React Native upgrade to 0.81 or later targets Android 16, what changes for edge-to-edge drawing and predictive back, and what do you verify?

level: seniorimportance: should knowfreq 33%

basics

~20 s

React Native 0.81 targets Android 16, which forces edge-to-edge drawing and turns on predictive back. Content now draws behind the system bars unless insets are applied, and BackHandler keeps working, but native onBackPressed() overrides may need migrating.

open as a page

In a React Native multi-step insurance-quote screen, why can a BackHandler listener that is never removed trap the user at step one or leave the Android back button dead after the flow closes?

level: seniorimportance: should knowfreq 40%

basics

~20 s

BackHandler keeps handlers in one global list until remove() is called. An effect that subscribes without cleanup leaves stale handlers; one that captured an old step returns true and swallows the press — at step one, or everywhere once the flow unmounts.

open as a page

In a React Native clinic-booking app whose date picker differs per platform, when do separate .ios and .android files beat inline Platform.select branches?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Split into per-platform files when the implementations differ in structure, state or dependencies, not just values; keep inline Platform.select for a few differing values. Keep the shared contract, logic and types in platform-neutral files so the split stays thin.

open as a page

A React Native forum app's share-and-report sheet works on iOS, but on Android it crashes or silently does nothing; how do the iOS-only APIs fail there, and how do you guard them?

level: seniorimportance: should knowfreq 28%

basics

~20 s

React Native's iOS-only APIs fail on Android in three ways: ActionSheetIOS and DynamicColorIOS throw, Alert.prompt returns silently, and Settings warns and returns null. Guard each call site with Platform.OS or hide them behind one cross-platform wrapper per capability.

open as a page

Reviewing a React Native weather app's header, you find Platform.OS ternaries on most style properties; what goes wrong, and how would you restructure it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Scattered ternaries hide each platform's full style, give every non-iOS target the Android value, and invite drift. Group differences into one Platform.select per StyleSheet entry, with shared properties outside, an explicit default, and runtime-varying values kept out.

open as a page

In React Native, what does the iOS-only Settings module store, and why does Settings.watchKeys not fire after your own Settings.set call?

level: middleimportance: nice to knowfreq 12%

basics

~20 s

Settings wraps iOS NSUserDefaults: get reads synchronously from a JavaScript copy, set writes through to native. watchKeys only reports changes made outside React Native code, such as the Settings app, because the native side ignores notifications caused by set.

open as a page

In React Native, how do Platform.isPad, Platform.isTV and Platform.isVision decide the device type, and what do they return on Android?

level: middleimportance: nice to knowfreq 25%

basics

~10 s

On iOS all three compare Platform.constants.interfaceIdiom with 'pad', 'tv' or 'vision'. On Android isTV checks uiMode === 'tv', isVision is always false, and isPad does not exist, so it reads as undefined.

open as a page