Skip to content

<NitroImage /> emits no load or error events, so expo-image's transition cannot be reproduced #175

Description

@mrousavy

Version

react-native-nitro-image@0.15.2.

The gap

<NitroImage /> emits no events at all. The generated view config is the whole surface:

nitrogen/generated/shared/json/NitroImageViewConfig.json

{
  "uiViewClassName": "NitroImageView",
  "supportsRawText": false,
  "bubblingEventTypes": {},
  "directEventTypes": {},
  "validAttributes": {
    "image": true,
    "resizeMode": true,
    "recyclingKey": true,
    "hybridRef": true
  }
}

Four props, zero events, and NativeNitroImageViewMethods extends HybridViewMethods {} is empty
too. So there is no onLoad, no onError, no onDisplay.

Why it matters beyond "a missing callback"

The concrete thing it blocks is a cross-fade. expo-image has transition={220}, a
cross-dissolve when the source changes, and react-native-fast-image and react-native's own
Image both at least give you onLoad so you can build one. With <NitroImage /> there is no
transition prop and no event to drive one yourself, so a source change is always a hard cut.

That is most visible in exactly the case this library is good at. If you are swapping the source
on a view that is already showing something - a progressively refined image, a low-res preview
replaced by the full one, a thumbnail replaced by the original - the swap is the moment the user
sees, and right now it can only pop.

useImage() is not a substitute. It tells you when a frame is ready, but using it means holding
the decoded Image in JS and passing image={image}, and the spec is explicit that this opts
out of the recycling path:

Image: Shows a specific in-memory Image instance. Even when the view goes invisible, the
image will still be in-memory.

So the only way to observe a load today is to give up the memory behaviour that made you reach
for the ImageLoader variant in the first place. (See also #124, which is about the retention
side of that same trade.)

Secondary losses from having no events: no way to know a load failed so you can show your own
fallback, no way to measure time-to-first-paint per image, and no way to tell "still loading"
apart from "loaded, but the image is genuinely blank".

Suggested fix

Direct events on the view, matching what the ecosystem already expects:

export interface NativeNitroImageViewProps extends HybridViewProps {
  // ...existing
  onLoad?: (image: { width: number; height: number }) => void
  onError?: (error: { message: string }) => void
}

onLoad fired after the bitmap is actually set on the native view (not when the promise
resolves, so it lines up with the frame the user sees) would be enough to build a fade in
Reanimated. onError fired from the catch that HybridImageView.onAppear already swallows
into a Log.e on Android would be a straight improvement over the current behaviour, which is
that a failed load is silent to the app.

A native transition prop would be nicer still for the common case, since the crossfade then
never crosses the bridge, but onLoad is the thing that unblocks people.

Context

Found while migrating Foodr off expo-image onto this
library (mrousavy/Foodr#19). The app streams generated dish photos - a preview frame lands in a
fraction of a second and is replaced by the finished image a few seconds later - and today
transition={220} is what makes that read as the picture sharpening rather than as the card
flickering. The migration PR is open and ships without the cross-fade, with a TODO at the call
site pointing here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions