React Native course ยท Module 6: Photos, camera and audio

expo-image - Optimal Image Display

2 min read ยท
In this lesson6

Welcome to the Crystal Gallery! You will learn how to handle multimedia in mobile applications.

Installing expo-image

npx expo install expo-image

Basic Usage

import { Image } from 'expo-image';
import { StyleSheet, View } from 'react-native';

function ImageGallery() {
  return (
    <View style={styles.container}>
      {/* Image from URL */}
      <Image
        source={{ uri: 'https://atlantis.app/images/crystal.jpg' }}
        style={styles.image}
        contentFit="cover"
        transition={300}
      />

      {/* Local image */}
      <Image
        source={require('@/assets/images/pearl.png')}
        style={styles.image}
        contentFit="contain"
      />

      {/* With placeholder and blur */}
      <Image
        source={{ uri: 'https://atlantis.app/images/large.jpg' }}
        placeholder={blurhash}
        style={styles.image}
        contentFit="cover"
        transition={500}
      />
    </View>
  );
}

const blurhash = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4';

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    width: '100%',
    height: 200,
    borderRadius: 12,
  },
});

contentFit (formerly resizeMode)

// Different fit modes
<Image source={source} contentFit="cover" />   // Fills, may crop
<Image source={source} contentFit="contain" /> // Fully visible, may have padding
<Image source={source} contentFit="fill" />    // Stretches to dimensions
<Image source={source} contentFit="none" />    // Original dimensions
<Image source={source} contentFit="scale-down" /> // contain or none (smaller)

Blurhash and Placeholder

import { Image } from 'expo-image';

// Blurhash - compact placeholder
const blurhash = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4';

function OptimizedImage() {
  return (
    <Image
      source={{ uri: 'https://example.com/image.jpg' }}
      placeholder={blurhash}
      placeholderContentFit="cover"
      style={styles.image}
      transition={500}
    />
  );
}

Caching

import { Image } from 'expo-image';

// Cache settings
<Image
  source={{ uri: imageUrl }}
  cachePolicy="memory-disk" // memory, disk, memory-disk, none
  style={styles.image}
/>

// Prefetching
import { Image } from 'expo-image';

async function preloadImages() {
  const urls = [
    'https://example.com/image1.jpg',
    'https://example.com/image2.jpg',
  ];

  await Promise.all(urls.map((url) => Image.prefetch(url)));
}

Image List with FlatList

interface ImageItem {
  id: string;
  uri: string;
  title: string;
}

function ImageList() {
  const [images, setImages] = useState<ImageItem[]>([]);

  return (
    <FlatList
      data={images}
      numColumns={2}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Pressable style={styles.imageCard}>
          <Image
            source={{ uri: item.uri }}
            style={styles.gridImage}
            contentFit="cover"
            transition={200}
          />
          <Text style={styles.imageTitle}>{item.title}</Text>
        </Pressable>
      )}
    />
  );
}

expo-image is significantly more performant than the standard Image from React Native!

Code for this lesson: App.tsx
import React, { useState } from 'react';
import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';
import { Image } from 'expo-image';

interface ImageItem {
  id: string;
  uri: string;
  title: string;
}

export default function App() {
  const [images] = useState<ImageItem[]>([
    { id: '1', uri: 'https://picsum.photos/200?1', title: 'Crystal' },
    { id: '2', uri: 'https://picsum.photos/200?2', title: 'Pearl' },
  ]);

  return (
    <FlatList
      data={images}
      numColumns={2}
      keyExtractor={(item) => item.id}
      contentContainerStyle={styles.grid}
      renderItem={({ item }) => (
        <Pressable style={styles.card}>
          {/* TODO: add an <Image> from expo-image that:
              - has source={{ uri: item.uri }}
              - uses the styles.image style
              - has contentFit="cover"
              - has transition={200} for a smooth fade-in */}

          <Text style={styles.title}>{item.title}</Text>
        </Pressable>
      )}
    />
  );
}

const styles = StyleSheet.create({
  grid: { padding: 8 },
  card: { flex: 1, margin: 4 },
  image: {
    width: '100%',
    height: 150,
    borderRadius: 12,
  },
  title: {
    color: '#aed6f1',
    textAlign: 'center',
    marginTop: 6,
  },
});

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which contentFit value fills the container, potentially cropping the image?

  2. 2. What is blurhash used for in expo-image?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange image prefetching:

  • Code editor

    Complete the FlatList code with expo-image.

Useful articles