skip to content

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.