Skip to content

Ignore stale image callbacks after an Image view is recycled on iOS - #58669

Closed
mattijsf wants to merge 1 commit into
react:mainfrom
mattijsf:fix/image-ignore-stale-callbacks-after-recycle
Closed

mattijsf wants to merge 1 commit into
react:mainfrom
mattijsf:fix/image-ignore-stale-callbacks-after-recycle

Conversation

@mattijsf

@mattijsf mattijsf commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Fixes #58667.

On iOS with the new architecture, an <Image> can show the image of another <Image> that was unmounted just before. RCTImageResponseObserverProxy dispatches its callbacks to the main queue. RCTImageComponentView used one proxy for its whole lifetime, and didReceiveImage only checked that the view still had a state. When a view was recycled and handed to a new <Image> while a callback for the old request was still queued, that callback was applied to the new image. If the new source was cached it had already been applied synchronously on subscribe, so the late old image stayed on screen.

This change:

  • creates a new RCTImageResponseObserverProxy for every subscription in _setStateAndResubscribeImageResponseObserver
  • ignores image, progress and failure callbacks whose observer is not the current proxy (the fromObserver: argument was already passed but not used). For failures this also stops a stale error from clearing the new image.

The proxy's address identifies the subscription. The new proxy is allocated before the previous one is released, so two consecutive subscriptions never share an address. Reuse of an old address would need two resubscriptions while a callback from the first one is still queued.

Changelog:

[IOS] [FIXED] - Image no longer shows the image of a previous source after its native view is recycled

Test Plan:

Reproducer: https://github.com/mattijsf/rn-image-recycled-view-stale-image (React Native 0.87.1, also as a Snack: https://snack.expo.dev/@mattijsf/image-recycle-stale-load-ios?platform=ios). It mounts 64 uncached remote images (red), replaces them with a cached one (green) as soon as the first red one has loaded, and counts a wrong image when the new image's onLoad reports the old image's size. A run is 50 rounds.

Release build on the iOS simulator (iPhone 17 Pro Max, iOS 26.5), React Native built from source (RCT_USE_PREBUILT_RNCORE=0), one run of 50 rounds each. The diff applies unchanged to 0.87.1.

rounds with a wrong image wrong images
0.87.1 27 of 50 120
0.87.1 with this change 0 of 50 0

With the change the red images still show up briefly before each swap, as intended, but none of them end up on a green tile.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 24, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 24, 2026

@javache javache left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this without enable_shared_from_this, it's generally an architectural smell.

Why isn't the comparison inside RCTImageComponentView enough? Casting this_ to a void * and passing that through should avoid any stale memory access (small risk of the memory getting re-used in place)

@mattijsf

mattijsf commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Can we do this without enable_shared_from_this, it's generally an architectural smell.

Why isn't the comparison inside RCTImageComponentView enough? Casting this_ to a void * and passing that through should avoid any stale memory access (small risk of the memory getting re-used in place)

Uthe fix is only in RCTImageComponentView now, and the reproducer still shows 0 wrong images.

Create a new observer proxy per subscription and ignore callbacks from
an older one, so a late image for a recycled view's previous source is
not applied.
@mattijsf
mattijsf force-pushed the fix/image-ignore-stale-callbacks-after-recycle branch from ca7d792 to f3a813a Compare September 24, 2026 13:09
@meta-codesync

meta-codesync Bot commented Sep 24, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D121619393.

@meta-codesync meta-codesync Bot closed this in 53bf98b Sep 25, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Sep 25, 2026
@meta-codesync

meta-codesync Bot commented Sep 25, 2026

Copy link
Copy Markdown

@javache merged this pull request in 53bf98b.

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS][Fabric] Image shows another Image's picture after its native view is recycled

2 participants