Image

Bundled images carry their own size, remote ones need an explicit width and height, and expo-image adds caching and transitions.

Syntax<Image source={{ uri }} style={{ width: 96, height: 96 }} resizeMode="cover" />

Image draws a bitmap: a bundled asset, a local file or a remote URL. There is no CSS background-image here, so a picture behind other content is an Image with absolutely positioned children over it, or the ImageBackground component.

Local and remote are not alike

source={require('./logo.png')} is resolved by the bundler at build time. The file's dimensions are known, so width and height may be left out. source={{ uri: 'https://...' }} is unknown until the download finishes, so you must set width and height in style. Without them the image lays out at zero size and draws nothing, and nothing is logged.

resizeMode

resizeMode takes cover (the default), contain, stretch, repeat or center. It can be written as a prop or as a key inside the style object; the two forms mean the same thing. It resembles CSS object-fit but it is a different property with different values.

expo-image

The expo-image package exports its own Image with memory and disk caching, and renamed props: contentFit in place of resizeMode, transition in milliseconds for a cross-dissolve as the image appears, cachePolicy (none, disk by default, memory, memory-disk) and placeholder for what shows while loading. Use it for screens full of remote pictures; the built-in Image is fine for bundled icons.

Example

Example · tsx
import { Image, StyleSheet, Text, View } from "react-native";

type AvatarRowProps = {
  name: string;
  photoUri: string | null;
};

export default function AvatarRow({ name, photoUri }: AvatarRowProps) {
  return (
    <View style={styles.row}>
      {photoUri === null ? (
        // require() is resolved by the bundler. The asset is 96x96, and that
        // intrinsic size is known at build time, so no style is needed.
        <Image source={require("./assets/avatar-fallback.png")} />
      ) : (
        // A remote image has no size until it has downloaded. Omit width and
        // height and it occupies nothing at all.
        <Image
          source={{ uri: photoUri }}
          style={styles.avatar}
          resizeMode="cover"
          accessible
          accessibilityLabel={`Photo of ${name}`}
          onError={({ nativeEvent: { error } }) =>
            console.log("avatar failed", error)
          }
        />
      )}
      <Text style={styles.name}>{name}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flexDirection: "row", alignItems: "center", gap: 12, padding: 12 },
  avatar: { width: 48, height: 48, borderRadius: 24 },
  name: { fontSize: 16, fontWeight: "500" },
});

// On screen: a circular 48pt photo with the name beside it. While the photo
// downloads the space stays blank at the right size, so nothing jumps. If the
// URL is null, the bundled fallback is drawn at its own 96pt size instead.

When to use it

  • Show a user avatar from an API, with a bundled image as the fallback.
  • Render a feed of remote photos that must stay smooth and not re-download on the way back.
  • Fit a wide photo into a fixed-size thumbnail without distorting it.
  • Place text over a photographic header using ImageBackground or an absolute overlay.

More examples

expo-image: caching, placeholder and a fade in

On screen: a blurred colour block appears immediately, then fades into the real poster over 200ms, and on a second visit it appears instantly from cache. This is the version to use in a feed of remote images; the built-in Image has no memory cache and no transition.

Example · tsx
// npx expo install expo-image
import { Image } from "expo-image";
import { StyleSheet, View } from "react-native";

// A tiny blurhash string, shown instantly while the real file downloads.
const PLACEHOLDER = "L6PjrWof00Dh00Dh00Dh00Dh00Dh";

type PosterProps = {
  uri: string;
};

export default function Poster({ uri }: PosterProps) {
  return (
    <View style={styles.frame}>
      <Image
        source={{ uri }}
        style={styles.poster}
        // contentFit is expo-image's name for resizeMode. Same idea,
        // different values: cover, contain, fill, none, scale-down.
        contentFit="cover"
        placeholder={{ blurhash: PLACEHOLDER }}
        // Milliseconds of cross-dissolve once the real image is ready.
        transition={200}
        // Default is "disk". "memory-disk" also keeps it in RAM for this run.
        cachePolicy="memory-disk"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  frame: { padding: 16 },
  poster: { width: "100%", aspectRatio: 2 / 3, borderRadius: 12 },
});

Ask the server for the pixels you will actually draw

A 48pt avatar needs 144 pixels on a 3x screen, not the 2000px original. Downloading full-size images is the usual cause of a list that stutters and an app that burns data. This helper is also the shape of code you can unit test, because it only returns a value.

Example · tsx
import { PixelRatio } from "react-native";
import type { ImageSourcePropType } from "react-native";

const FALLBACK: ImageSourcePropType = require("./assets/avatar-fallback.png");

// Pure helper, no UI. Points are layout units; a device draws
// PixelRatio.get() physical pixels per point (2 or 3 on most phones).
export function avatarSource(
  url: string | null,
  sizeInPoints: number,
  pixelRatio: number,
): ImageSourcePropType {
  if (url === null) {
    return FALLBACK;
  }
  const px = Math.round(sizeInPoints * pixelRatio);
  return { uri: `${url}?w=${px}&h=${px}&fit=crop` };
}

console.log(avatarSource("https://cdn.example.com/u/7", 48, 3));
// Output: { uri: 'https://cdn.example.com/u/7?w=144&h=144&fit=crop' }

console.log(avatarSource(null, 48, 3) === FALLBACK);
// Output: true

// In a component: avatarSource(user.photo, 48, PixelRatio.get())
export const DEVICE_SCALE = PixelRatio.get();

The same file in every resizeMode

On screen: four fixed 140x100 boxes holding a wide photo. cover fills the box and crops the sides, contain fits it whole and leaves grey bars, stretch distorts it, center draws it at native size clipped by the box. Keep this handy when deciding between cover and contain for a thumbnail.

Example · tsx
import { Image, StyleSheet, Text, View } from "react-native";
import type { ImageResizeMode } from "react-native";

const MODES: ImageResizeMode[] = ["cover", "contain", "stretch", "center"];
const URI = "https://picsum.photos/id/237/800/400";

export default function ResizeModeDemo() {
  return (
    <View style={styles.grid}>
      {MODES.map((mode) => (
        <View key={mode} style={styles.cell}>
          {/* Equivalent to writing resizeMode inside the style object:
              style={[styles.box, { resizeMode: mode }]} */}
          <Image source={{ uri: URI }} style={styles.box} resizeMode={mode} />
          <Text style={styles.caption}>{mode}</Text>
        </View>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  grid: { flexDirection: "row", flexWrap: "wrap", gap: 12, padding: 12 },
  cell: { gap: 4 },
  box: {
    width: 140,
    height: 100,
    backgroundColor: "#e4e4e7",
    borderRadius: 6,
  },
  caption: { fontSize: 12, color: "#71717a", textAlign: "center" },
});

Discussion

  • Be the first to comment on this lesson.