DEV Community

Cathy Lai
Cathy Lai

Posted on

Image Load Races in React Native - Fix It in One Line

When users tap quickly between images, your preview can flicker, hide the spinner at the wrong time, or even show the wrong photo. That’s a classic race condition during image loading. Here’s the core fix in one line—and why it works.

The problem

You show a full-screen preview of an image in a modal (or a floating box). A user taps Image A, it starts loading. Before it finishes, they tap Image B. If Image A completes late, its onLoad/onLoadEnd can still fire and mutate state that now “belongs” to Image B—hiding the spinner too early or causing flicker.

Why it happens

React Native’s Image kicks off a native request as soon as it renders with source={{ uri }}. If you keep the same Image instance mounted while swapping the URI, late callbacks from the previous load can still arrive and update your UI state.

The key solution: “latest selection wins” via force-remount

Force the preview Image to remount whenever the selection changes by adding a key tied to the selected image’s ID. When a new image is picked, the old preview unmounts—its native request is dropped and its callbacks won’t run—so only the current Image controls the spinner.

Fix

<Modal
  visible={modalVisible}
  onRequestClose={() => setModalVisible(false)}
>
    <View style={styles.modalContent}>
      <Image
        key={image?.id || "placeholder"} /// <-- FIX : latest selection wins 
        source={{ uri: image?.urls?.full || "https://placehold.co/600x400" }}
        ...
        onLoadStart={() => setModalLoading(true)}
        onLoadEnd={() => setModalLoading(false)}
      />
    </View>

  {modalLoading && (
    <View style={styles.skeleton}>
      <ActivityIndicator size="large" />
      <Text style={styles.skeletonText}>Loading...</Text>
    </View>
  )}
</Modal>
Enter fullscreen mode Exit fullscreen mode

Deep dive: what the Image actually does

Yes. React Native’s Image starts a native image request (Fresco/Glide on Android, SDWebImage/URLSession on iOS) when you render it with a source={{ uri }}. It’s not the JS fetch() API, so there’s no AbortController you can call from JS.

What you can rely on:

  • Unmount/source change cancels natively: If you unmount the Image or change its key/source, the native layer cancels/drops the old request and callbacks. That’s why using key={image.id} is effective against races.
  • No JS abort: You can’t abort a still-mounted Image request from JS; instead, remount or ignore late events (token guard).
  • Prefetch alternative: Image.prefetch(url) can warm the cache, but it also can’t be aborted; you still need a “latest selection wins” guard.

If you need more control, libraries like expo-image/FastImage improve caching and cancel on unmount/source change as well, but they still don’t expose a JS abort handle for an in-place load.

Takeaway

For image previews where users can rapidly change selections, make the latest selection win by remounting the preview Image with key={selectedImage.id} and drive the loader off onLoadStart/onLoadEnd. This prevents late callbacks from earlier loads from ever touching your current UI state.

Top comments (0)