In React Native, how does Image pick between icon.png, [email protected] and [email protected], and why must the require() path be static?
answer
- one require, several files
- suffixes name the screen density
- closest match when none is exact
- resolved at bundle time, not runtime
- choose between two require calls
basics
~20 sRequire the base name once; the device's screen density picks the @2x or @3x variant, falling back to the closest one. The path must be a literal string because the bundler resolves and packages assets at bundle time, not at runtime.
solid answer
~40 sPut `bed.png`, `[email protected]` and `[email protected]` side by side and write `require('./icons/bed.png')`. The bundler records all the variants, and at runtime the image matching the device's pixel density is used; if there is no exact match, the closest best option is chosen. That is why one `require` works on a 2x phone and a 3x phone without code changes. Resolution happens when the app is bundled: the bundler scans for `require` calls with literal paths and packages only the images actually referenced. A path built at runtime, such as `require('./icons/' + name + '.png')`, cannot be resolved and fails. To choose an icon dynamically, write each `require` with a literal path and pick between the results, for example in a lookup object.
code
tsx · 15 linesimport {Image} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
// Every path is a literal, so the bundler can package each icon with its @2x/@3x variants.
const AMENITY_ICONS: Record<string, ImageSourcePropType> = {
bed: require('./icons/bed.png'),
bath: require('./icons/bath.png'),
parking: require('./icons/parking.png'),
};
export function AmenityIcon({name}: {name: string}) {
const icon = AMENITY_ICONS[name];
return icon ? <Image source={icon} /> : null;
// Broken alternative: require('./icons/' + name + '.png') cannot be resolved at bundle time.
}go deeper
Recall the @2x and @3x naming, that you require only the base name, and that require paths must be literal strings.
Explain that assets are resolved and packaged at bundle time, how the closest density is chosen, and the lookup-object pattern for dynamic choice.
Organise icon assets so dynamic selection stays bundler-friendly, keep app size in check, and know when an iOS asset catalog is worth enabling.
Decide which imagery ships in the binary versus from a server or vector source, weighing app size, offline use and release cadence.
## Density variants Phone screens pack different numbers of physical pixels into each layout point. An icon drawn at 24 x 24 points needs 48 x 48 pixels on a 2x screen and 72 x 72 on a 3x screen to look sharp. React Native handles this with **file-name suffixes**: ``` icons/ bed.png // 1x [email protected] // 2x [email protected] // 3x ``` You reference only the base name: ```tsx <Image source={require('./icons/bed.png')} /> ``` The bundler knows about every variant and the image matching the device's screen density is used. **If there is no exact match, the closest best option is selected**, so a project that ships only `@2x` and `@3x` still works on other densities. The `Image` is laid out at the asset's size in points, so the 2x and 3x files render at the same on-screen size, just sharper. ## Why the path must be static Bundled images are not loaded from disk by name at runtime. When the app is bundled, the bundler walks the code, finds each `require` with a **literal path**, resolves it like a JavaScript module, records the asset's size and density variants, and packages it with the app. Several useful properties follow: 1. The same system works on iOS and Android. 2. Images live next to the components that use them, with no global name collisions. 3. **Only images actually referenced are packaged**, which keeps the app smaller. 4. The asset's width and height are known, so the `Image` sizes itself. The price is that the path must be known when bundling: | Code | Works? | Why | |---|---|---| | `require('./icons/bed.png')` | yes | literal path, resolved at bundle time | | `require('./icons/' + name + '.png')` | no | the path only exists at runtime | | `const icons = {bed: require('./icons/bed.png'), bath: require('./icons/bath.png')}` then `icons[name]` | yes | every path is literal; the choice happens at runtime | | `active ? require('./on.png') : require('./off.png')` | yes | two literal requires, one chosen | ## A listing gallery example A real-estate listing shows amenity icons from bundled assets next to remote property photos. The API returns amenity names such as `"bed"`, `"bath"` and `"parking"`. The icons are mapped once: - a module exports an object from amenity name to `require(...)` result; - the row looks up `icons[amenity]` and renders a fallback when the name is unknown; - new amenities need a new file and a new entry, which is a code change reviewed with the design. ## Common mistakes - **Shipping only a 1x file.** It renders at the right size in points but looks blurry on 2x and 3x screens. - **Inconsistent variants.** A `@3x` file that is not exactly three times the 1x dimensions renders at the wrong size or looks misaligned next to its siblings. - **Renaming a file without updating the `require`.** The bundler fails the build rather than rendering a blank image, which is the safer failure. - **Treating the result of `require` as a path.** It is an opaque reference; pass it to `source` or to `Image.resolveAssetSource`, not to string functions. ## Delivery details worth knowing - By default on iOS each required image and **every `@2x`/`@3x` variant** is copied into the app as a loose file next to the JavaScript bundle. Setting `RCTUseAssetCatalog` to `true` in `Info.plist` compiles them into an asset catalog instead, so the system can ship only the scale a device needs; a clean build is needed after changing it. - **In development**, images are served by the bundler, so adding or changing an image does not need a native rebuild; refreshing is enough. - **Remote images have no density variants**; the server decides what it sends. The `source` array form, or `srcSet`, lets you offer several URLs and have the native side pick one for the container's size or pixel density. - The same `require` mechanism also packages other media such as audio, video and PDF files; how the bundler resolves asset paths and extensions is its own topic.
- In React Native, what happens when a 3x device needs icon.png but the project only ships icon.png and [email protected]?The docs say the closest best option is selected when no variant matches the screen density, so the device uses `[email protected]`, scaled to the icon's point size. It still displays at the right size in points, but it can look slightly soft on a 3x screen. Shipping a `@3x` variant fixes that.
- In React Native, why does a lookup object of require() calls work when require('./icons/' + name + '.png') does not?The bundler resolves `require` calls with literal paths when it builds the bundle. In the lookup object every path is written out, so each asset is found, measured and packaged; choosing an entry at runtime only picks between already-resolved references. A concatenated path only exists at runtime, so there is nothing for the bundler to resolve.
saying these in an interview costs you the question
- You must require('./[email protected]') explicitly on high-density phones.
- require() paths can be built at runtime like any string.
- Missing a density variant makes the Image fail to render.
- Every image in the project folder is packaged, used or not.
- @2x and @3x files render at two and three times the on-screen size.