Blog Mobile UX 7 min read

Expo Image Component Fallback and Placeholder for Remote URLs

Configure expo image fallback using the Expo Image component's placeholder prop and onError event for remote URLs in React Native apps.

expo image fallbackExpo Image componentReact Native placeholderblurhash placeholderExpo SDK
Expo Image Component Fallback and Placeholder for Remote URLs

The Expo Image component (expo-image) is a high-performance Image implementation for React Native that provides a placeholder prop, transition animations, and onError event handling out of the box. It handles the iOS vs Android defaultSource inconsistency that plagues the built-in Image component and adds disk caching with configurable cache policies.

Using a fallback.pics URL as the expo image fallback eliminates bundled placeholder assets and lets you generate dimension-matched placeholders dynamically. The component's blurhash placeholder support is also useful when you have a blurhash string from your image CDN.

Component API

Expo Image placeholder, source, and onError props

The Expo Image component's placeholder prop accepts a blurhash string, a thumbhash string, a local asset (require()), or a remote URL. The placeholder is shown while the source image is loading. When loading completes (success or error), the placeholder transitions out.

The source prop accepts a URI string, a local asset, or an array of sources for progressive loading (low-res first, high-res on network load). The onError callback fires when the source fails to load. Expo Image does not automatically switch to a fallback — you update the source in state inside onError.

The transition prop controls the animation from placeholder to loaded image. Use { duration: 200 } for a 200ms cross-fade. Expo Image also supports { timing: 'ease-in-out' } for more control over the animation curve.

Implementation tsx
// Expo Image with placeholder and onError fallback
import { Image } from 'expo-image';
import { useState } from 'react';

const FALLBACK = (w: number, h: number) =>
  `https://fallback.pics/api/v1/${w}x${h}/E4E4E7/71717A`;

export function ExpoImageWithFallback({
  uri,
  width = 200,
  height = 200,
}: {
  uri: string;
  width?: number;
  height?: number;
}) {
  const [errored, setErrored] = useState(false);

  return (
    <Image
      source={errored ? FALLBACK(width, height) : uri}
      placeholder={{ uri: `https://fallback.pics/api/v1/${width}x${height}/E4E4E7/A1A1AA` }}
      transition={200}
      style={{ width, height }}
      contentFit="cover"
      onError={() => {
        if (!errored) setErrored(true);
      }}
    />
  );
}

Blurhash placeholder

Using blurhash strings as expo image placeholder

Blurhash is a compact representation of an image's color and structure, encoded as a short string. When you have a blurhash string from your image CDN (Cloudinary and imgix both provide them), you can pass it directly to Expo Image's placeholder prop. The component decodes the blurhash and renders a blurred preview while the full image loads.

When you do not have a blurhash string, a URL-based placeholder is the next best option. The placeholder URL shows a solid color or simple pattern that matches the image's expected dimensions. A grey rectangle is the minimal useful placeholder — it reserves space and prevents layout shift without implying any content.

Implementation tsx
// Blurhash placeholder when available, URL placeholder as fallback
import { Image } from 'expo-image';

export function SmartPlaceholderImage({
  uri,
  blurhash,
  width,
  height,
}: {
  uri: string;
  blurhash?: string;
  width: number;
  height: number;
}) {
  const [errored, setErrored] = useState(false);
  const placeholder = blurhash
    ? blurhash
    : { uri: `https://fallback.pics/api/v1/${width}x${height}/E4E4E7/A1A1AA` };

  return (
    <Image
      source={errored
        ? `https://fallback.pics/api/v1/${width}x${height}/7C3AED/FFFFFF`
        : uri}
      placeholder={placeholder}
      transition={300}
      style={{ width, height }}
      contentFit="cover"
      onError={() => { if (!errored) setErrored(true); }}
    />
  );
}

Cache policy

Expo Image caching and when remote fallbacks are fetched

Expo Image caches images on disk by default. The cachePolicy prop controls this: 'memory-disk' (default, best performance), 'memory' (only RAM, lost on app restart), 'disk' (only disk, no RAM cache), or 'none' (no cache). For most apps, the default is correct.

The remote fallback URL from fallback.pics is also cached by Expo Image after the first fetch. Subsequent onError events for the same primary URL use the cached fallback image. This is desirable — the fallback.pics CDN returns Cache-Control: public, max-age=31536000, so the cached entry is valid for a year.

If you use the same fallback URL for many images (same dimensions, same color), Expo Image hits the disk cache for every image after the first one. This is efficient. If you generate unique fallback URLs per image (including the product name in text parameter), each URL is a separate cache entry.

Avatar fallback

Expo Image for user avatar placeholders with initials

User avatars are a common use case for expo image fallback. When a user has not uploaded a profile photo, or the photo URL becomes unavailable, the avatar shows a broken image or a default grey circle. A fallback.pics avatar URL with the user's initials provides a useful state.

Generate the avatar fallback URL from the user's name. Use the /avatar/ route with the text parameter set to the user's initials. The avatar route renders a circular image suitable for round avatar displays. For square avatar containers, use /square/ instead.

Implementation tsx
// Avatar with initials fallback
import { Image } from 'expo-image';

function getInitials(name: string): string {
  return name
    .split(' ')
    .slice(0, 2)
    .map((n) => n[0]?.toUpperCase() ?? '')
    .join('');
}

export function UserAvatar({ avatarUrl, name, size = 48 }: {
  avatarUrl?: string;
  name: string;
  size?: number;
}) {
  const [errored, setErrored] = useState(!avatarUrl);
  const initials = getInitials(name);
  const fallback = `https://fallback.pics/api/v1/avatar/${size}?text=${initials}`;

  return (
    <Image
      source={errored || !avatarUrl ? fallback : avatarUrl}
      style={{ width: size, height: size, borderRadius: size / 2 }}
      contentFit="cover"
      onError={() => { if (!errored) setErrored(true); }}
    />
  );
}

vs React Native Image

When to use Expo Image over the built-in Image component

Use expo-image when you need: consistent behavior across iOS and Android, built-in disk caching, blurhash or thumbhash placeholder support, smooth cross-fade transitions, or better performance in large image lists. The trade-off is adding an Expo SDK dependency.

Use the built-in React Native Image when you need a smaller dependency footprint and your image use case is simple (no progressive loading, no disk cache required, no animations). For apps already using Expo SDK, expo-image is almost always the better choice.

Key takeaways

What to standardize before shipping

  • Expo Image's placeholder prop accepts blurhash strings, thumbhash strings, local assets, or remote URLs — use a fallback.pics URL when no blurhash is available.
  • Expo Image handles iOS/Android defaultSource inconsistency correctly, making it preferable to the built-in Image component.
  • Cache the remote fallback URL via Expo Image's disk cache — subsequent requests for the same fallback dimensions hit the cache without a network round trip.
  • Generate avatar fallbacks from user initials via the /avatar/ route for a useful non-empty state when profile photos are unavailable.
  • Use cachePolicy: 'memory-disk' (default) for best performance; fallback.pics CDN headers allow year-long cache entries.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs