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 }}
/>
Important detail: A network image requires explicit 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:

resizeModeWhat it does visually
coverScales the image to fill the box entirely, cropping any overflow — keeps aspect ratio, may cut off edges
containScales the image to fit entirely inside the box, keeping its full content visible — keeps aspect ratio, may leave empty space
stretchStretches the image to exactly fill the box in both dimensions — does not keep aspect ratio, can distort the image
centerCenters 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.

Next up: With images and fonts covered, the next lesson looks at code that needs to behave differently per platform: Platform-Specific Code.