From f3a813af4b73a707899694e817248ab5600ad19e Mon Sep 17 00:00:00 2001 From: mattijsf Date: Thu, 24 Sep 2026 15:09:18 +0200 Subject: [PATCH] Ignore stale image callbacks after an Image view is recycled on iOS 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. --- .../Image/RCTImageComponentView.mm | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index 40cfa512e5c4..25f9eec7fd82 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -38,8 +38,6 @@ - (instancetype)initWithFrame:(CGRect)frame _imageView.layer.minificationFilter = kCAFilterTrilinear; _imageView.layer.magnificationFilter = kCAFilterTrilinear; - _imageResponseObserverProxy = std::make_shared(self); - self.contentView = _imageView; } @@ -124,6 +122,11 @@ - (void)_setStateAndResubscribeImageResponseObserver:(const ImageShadowNode::Con _state = state; if (_state) { + // A new observer per subscription: callbacks of a previous request can still be queued on the + // main queue (e.g. after this view was recycled and reused), and must not be applied here. + // The callbacks are matched by the proxy's address. The new proxy is allocated before the + // previous one is released, so two consecutive subscriptions never share an address. + _imageResponseObserverProxy = std::make_shared(self); auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator(); observerCoordinator.addObserver(_imageResponseObserverProxy); } @@ -140,8 +143,9 @@ - (void)prepareForRecycle - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer { - if (!_eventEmitter || !_state) { - // Notifications are delivered asynchronously and might arrive after the view is already recycled. + if (!_eventEmitter || !_state || observer != _imageResponseObserverProxy.get()) { + // Notifications are delivered asynchronously and might arrive after the view is already recycled, + // or after it has been reused for another image. // In the future, we should incorporate an `EventEmitter` into a separate object owned by `ImageRequest` or `State`. // See for more info: T46311063. return; @@ -187,7 +191,7 @@ - (void)didReceiveProgress:(float)progress total:(int64_t)total fromObserver:(const void *)observer { - if (!_eventEmitter) { + if (!_eventEmitter || observer != _imageResponseObserverProxy.get()) { return; } @@ -196,6 +200,10 @@ - (void)didReceiveProgress:(float)progress - (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer { + if (observer != _imageResponseObserverProxy.get()) { + return; + } + _imageView.image = nil; if (!_eventEmitter) {