skip to content

In a React Native listing gallery, how do you show a placeholder while a remote Image loads and a fallback when it fails?

level: middleimportance: should knowfreq 42%

answer

  1. four load callbacks, one fires either way
  2. onLoad carries the source size
  3. onError carries an error string
  4. defaultSource is ignored in Android debug
  5. fallback must be bundled, not remote

basics

~20 s

Use defaultSource for a bundled placeholder during loading, and onError to swap to a bundled fallback when the download fails. onLoadStart, onLoad and onLoadEnd track progress. On Android, defaultSource is ignored in debug builds, so test the placeholder in a release build.

solid answer

~40 s

`Image` reports its lifecycle: `onLoadStart` when loading begins, `onLoad` on success with `nativeEvent.source` holding `width`, `height` and `uri`, `onError` on failure with `nativeEvent.error`, and `onLoadEnd` after either. For the placeholder, `defaultSource` takes a static image shown while `source` loads; on Android it is **ignored in debug builds**, so it only appears in release builds. For failures, keep a `failed` flag set in `onError` and render a bundled `require()` fallback, never another remote URL that can fail too. `loadingIndicatorSource` is an alternative placeholder, but on Android an `Image` with both it and `defaultSource` throws. Android also fades images in over `fadeDuration`, 300 ms by default.

code

tsx · 26 lines
tsx
import {useState} from 'react';
import {Image, StyleSheet} from 'react-native';

const PLACEHOLDER = require('./images/house-placeholder.png');
const FALLBACK = require('./images/photo-unavailable.png');

// Render as <ListingPhoto key={uri} uri={uri} /> so a new URL starts with a fresh failed flag.
export function ListingPhoto({uri}: {uri: string}) {
  const [failed, setFailed] = useState(false);

  return (
    <Image
      source={failed ? FALLBACK : {uri}}
      defaultSource={PLACEHOLDER} // ignored in Android debug builds
      onError={e => {
        console.warn('listing photo failed', e.nativeEvent.error);
        setFailed(true);
      }}
      style={styles.photo}
    />
  );
}

const styles = StyleSheet.create({
  photo: {width: '100%', aspectRatio: 4 / 3},
});

go deeper

for a junior

Recall onLoadStart, onLoad, onError and onLoadEnd, and that defaultSource shows a static image while loading.

for a middle

Explain what each event carries, why onLoadEnd suits cleanup, and the Android defaultSource debug-build and loadingIndicatorSource rules.

for a senior

Build thumbnails that keep a stable frame, fall back to bundled assets, reset state when the URL changes and never retry in a loop.

for a principal

Define how the product represents missing media and how failures are surfaced to monitoring, so broken listings are fixed at the source.

## The load lifecycle A remote `Image` goes through a download and decode before it can show pixels. React Native exposes that as callbacks: | Callback | When | Payload | |---|---|---| | `onLoadStart` | loading begins | none | | `onProgress` (iOS) | as data arrives | progress event | | `onLoad` | loading succeeded | `nativeEvent.source` with `width`, `height`, `uri` | | `onError` | loading failed | `nativeEvent.error` | | `onLoadEnd` | after success **or** failure | none | `onLoadEnd` is the place to clear a spinner, because it runs whichever way the load ended. `onLoad`'s `source` size is the loaded image's size, useful for adjusting an aspect ratio after the fact; but laying out from it causes a jump, so prefer ratios known up front. ## Placeholders while loading Three options, from simplest to most flexible: 1. **`defaultSource`**: a static image displayed while `source` loads, typically a bundled `require()` of a neutral house silhouette. 2. **`loadingIndicatorSource`**: a resource shown as a loading indicator until the image is ready. 3. **Your own overlay**: a `View` or `ActivityIndicator` layered over the image area, hidden in `onLoadEnd`. Platform details that cause real bugs: - **On Android, `defaultSource` is ignored on debug builds.** A developer testing in debug sees no placeholder and assumes the prop is broken; it appears in release builds. - **On Android, `defaultSource` and `loadingIndicatorSource` cannot be combined**: the `Image` throws an error telling you to use one or the other. - **On Android, images fade in** over `fadeDuration`, 300 ms by default; set it to `0` for instant swaps such as a gallery that pages quickly. ## Fallbacks on failure Listing photos fail for ordinary reasons: a removed photo, an expired signed URL, no connectivity. A robust pattern: - keep a `failed` state per image; - set it in `onError`; - when `failed` is true, render a **bundled** fallback with `require()`, which cannot fail to download; - optionally show a small "photo unavailable" caption; - reset `failed` when the `uri` changes, for example by keying the component on the URL. Two mistakes to avoid: 1. **A remote fallback URL.** If it also fails, `onError` fires again and the component can bounce between sources. 2. **Retrying in a tight loop from `onError`.** Retry deliberately, with a user action or a back-off, not on every error event. ## Layout stays stable throughout Placeholders only work if the frame does not change size when the real image arrives. Give the `Image` its final dimensions or aspect ratio from the start, so the placeholder, the photo and the fallback all occupy the same box. Otherwise the gallery jumps each time a state changes. ## Testing the states Each state needs to be seen on purpose, because a fast connection in development hides most of them: 1. **Slow load**: throttle the network in the simulator or emulator settings and confirm the placeholder appears and the frame does not move. 2. **Failure**: point one listing at a URL that returns 404 and confirm the bundled fallback renders once, not repeatedly. 3. **Android release build**: confirm `defaultSource` is visible, since debug builds ignore it. 4. **URL change**: scroll or navigate so the same component receives a new URL after a failure, and confirm the new photo loads instead of the old fallback sticking. ## A checklist for a listing thumbnail - fixed frame from the design (`width` and `aspectRatio`); - `defaultSource={require('./placeholder.png')}`, verified in a release build on Android; - `onError` switching to a bundled fallback; - `onLoadEnd` clearing any overlay spinner; - `fadeDuration={0}` on Android if the fade feels slow while paging.

  • In React Native, why does an Image's defaultSource never appear when you test on an Android debug build?
    The docs state that on Android the `defaultSource` prop is ignored on debug builds. The placeholder works in release builds, so verify it there, or use your own overlay placeholder if you need it visible during development.
  • In React Native, why should an Image's error fallback be a require() asset rather than another URL?
    A bundled asset ships inside the app and cannot fail to download, so the fallback always renders. A second remote URL can fail for the same reasons as the first, such as no connectivity, which fires `onError` again and can leave the component switching between sources or showing nothing.

saying these in an interview costs you the question

  • onLoadEnd fires only when the image loaded successfully.
  • defaultSource shows on every Android build, including debug.
  • An Image can use both defaultSource and loadingIndicatorSource on Android.
  • onError should immediately retry the same URL until it succeeds.
  • A placeholder removes the need to give the Image a fixed size.