Images & Assets
Every React Native app needs images — logos, icons, avatars, photos loaded from a server. The
Image component handles all of these, but bundled local images and network images
behave differently in a few important ways that are easy to get wrong the first time. This
lesson covers displaying both correctly, controlling how an image fills its box, handling load
errors, and a couple of related topics — custom fonts and Expo's asset system — that follow the
same "declare it as an asset" idea.
Displaying a Bundled Local Image With require()
An image that ships inside your app (a logo, an icon) is referenced with
require(), passed directly to the source prop:
import React from 'react';
import { Image } from 'react-native';
function Logo() {
return <Image source={require('./assets/logo.png')} style={{ width: 120, height: 120 }} />;
}
It's worth understanding why this is require('./assets/logo.png') and not
simply a string path (the way, say, Flutter's asset system takes a string like
'assets/logo.png' declared in a config file). require() is resolved by
React Native's JavaScript bundler (Metro) at build time, not at runtime — the
bundler statically scans your code, finds every require() call pointing at an image,
and packages that exact file into the app, generating the right reference for every screen
density along the way (so a high-density device automatically gets a sharper version if you've
provided one, such as logo@2x.png). A plain string path has none of that build-time
guarantee — the bundler wouldn't know the file needs to be packaged at all.
Displaying a Network Image
For an image hosted remotely, pass an object with a uri key instead of using
require():
<Image
source={{ uri: 'https://example.com/photos/profile.jpg' }}
style={{ width: 100, height: 100 }}
/>
width and
height styling. For a bundled local image, React Native can read the file's
actual dimensions at build time (from the file itself), so it can size the image sensibly even
without an explicit style. A remote URL has no such build-time information — React Native has no
way to know the photo behind that URL is, say, 800x600 until it has already started downloading
it — so without an explicit size, a network Image renders as invisible (zero-sized).
Always set a size for network images.
Controlling How an Image Fills Its Box: resizeMode
When an image's own aspect ratio doesn't match the box you've sized it into, the
resizeMode prop controls how it's fit — the same idea as Flutter's
BoxFit, with different names:
| resizeMode | What it does visually |
|---|---|
cover | Scales the image to fill the box entirely, cropping any overflow — keeps aspect ratio, may cut off edges |
contain | Scales the image to fit entirely inside the box, keeping its full content visible — keeps aspect ratio, may leave empty space |
stretch | Stretches the image to exactly fill the box in both dimensions — does not keep aspect ratio, can distort the image |
center | Centers the image at its natural size — no scaling up, but will shrink down if the image is larger than the box |
<Image
source={{ uri: 'https://example.com/photos/banner.jpg' }}
style={{ width: '100%', height: 180 }}
resizeMode="cover"
/>
Handling Loading and Errors: onLoad and onError
Network images take time to download, and can fail (a bad URL, no connection). The
Image component exposes onLoad and onError callback props
so you can show a placeholder while waiting, and something sensible if the image never arrives:
import React, { useState } from 'react';
import { View, Image, ActivityIndicator, Text } from 'react-native';
function Avatar({ uri }) {
const [loading, setLoading] = useState(true);
const [failed, setFailed] = useState(false);
if (failed) {
return <Text>Couldn't load image</Text>;
}
return (
<View style={{ width: 80, height: 80 }}>
{loading && <ActivityIndicator />}
<Image
source={{ uri }}
style={{ width: 80, height: 80, borderRadius: 40 }}
onLoad={() => setLoading(false)}
onError={() => { setLoading(false); setFailed(true); }}
/>
</View>
);
}
Expo's Asset System: expo-asset
If you're building with Expo, expo-asset lets you preload images
(and other assets) before your first screen renders, so the user never sees a brief flash of a
missing image while it loads in the background. This is typically done once, during your app's
startup/splash sequence, by calling Asset.loadAsync([...]) with the list of images
your app needs early, and only rendering the main UI once that promise resolves. This is a brief,
conceptual mention — the exact setup depends on your navigation/splash-screen structure, but the
underlying idea is the same as bundling: make sure the asset is fully ready before it's needed.
Custom Fonts
Loading a custom font follows the same underlying principle as images — the font file has to be
registered as an asset before you can reference its name in a style. In an Expo-managed project,
the useFonts hook from expo-font handles this, resolving once the fonts
have finished loading:
import { useFonts } from 'expo-font';
function App() {
const [fontsLoaded] = useFonts({
'Poppins-Regular': require('./assets/fonts/Poppins-Regular.ttf'),
});
if (!fontsLoaded) {
return null; // or a loading screen
}
return <Text style={{ fontFamily: 'Poppins-Regular' }}>Styled with a custom font</Text>;
}
In a bare (non-Expo) workflow, custom fonts are linked manually into the native iOS and Android projects instead of being loaded through JavaScript at runtime — a native configuration step outside the scope of this lesson, but conceptually the same goal: make the font file part of the app's bundled assets before any component tries to use it by name.
A Note on Better Network Image Performance: react-native-fast-image
The built-in Image component is fine for most apps, but for screens with many
network images — a photo grid, a social feed — its caching behavior can be limited. A popular
third-party package, react-native-fast-image, offers more aggressive, more
configurable image caching and generally smoother performance for image-heavy screens. This is
conceptual awareness only for this lesson — much like cached_network_image in the
Flutter world — worth knowing exists once you notice repeated network images reloading or
flickering more than expected with the built-in component.