Skip to content

RefreshControl Issues - tintColor prop not respected & gets stuck on navigation #53987

Description

@ChristopherGabba

Description

There are two bugs I'm reporting with the same component, and providing a good repro for each within the same repository.
This issue is the single main problem I am having throughout my app with the new architecture. I use this RefreshControl component everywhere and it is not behaving right anywhere.

Configuration:

  • New Arch
  • RN 0.81.4
  • Expo 54.0.10
  • React 19.1.0
  • iOS

Bug 1 - tintColor property is not respected:

Code:

          <RefreshControl
            refreshing={refreshing}
            onRefresh={manualRefresh}
            tintColor="orange" // Should be orange but isn't respected
          />

Video Displaying Issue:

ScreenRecording_09-30-2025.05-35-09_1.MP4

There is a hack around this issue by applying the tintColor after a small delay:

  const [tintColor, setTintColor] = useState<ColorValue>("white")
  useEffect(() => {
    setTimeout(() => setTintColor("orange"), 500)
  }, [])

Bug 2 - Refresh Control gets stuck on navigation:

Changing tabs while a pull-to-refresh is active makes the RefreshControl get stuck.

The only hack around this that I have found is force-resetting the refreshing prop after the screen is unfocused.

const isFocused = useIsFocused()

useEffect(() => {
    if (!isFocused) {
        setRefreshing(false)
    }
},[isFocused])

Video Displaying Issue:

ScreenRecording_09-30-2025.05-31-08_1.MP4

Note I have found several past issues that are somewhat related:

#46631 (Closed, but probably shouldn't be)
#51914
#35779

Steps to reproduce

Download the repro found here:
https://github.com/ChristopherGabba/RNV7_TestApp

Run:

  1. yarn install
  2. npx expo prebuild --clean
  3. npx expo run:ios --device

React Native Version

0.81.4

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

info Fetching system and libraries information...
System:
  OS: macOS 15.6.1
  CPU: (10) arm64 Apple M2 Pro
  Memory: 131.34 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 23.4.0
    path: ~/.nvm/versions/node/v23.4.0/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 11.5.2
    path: ~/.nvm/versions/node/v23.4.0/bin/npm
  Watchman:
    version: 2025.04.14.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /Users/christophergabba/.gem/ruby/3.4.3/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.0
      - iOS 26.0
      - macOS 26.0
      - tvOS 26.0
      - visionOS 26.0
      - watchOS 26.0
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.1 AI-251.26094.121.2513.14007798
  Xcode:
    version: 26.0.1/17A400
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.11
    path: /usr/bin/javac
  Ruby:
    version: 3.4.3
    path: /Users/christophergabba/.rubies/ruby-3.4.3/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.4
    wanted: 0.81.4
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Stacktrace or Logs

N/A

MANDATORY Reproducer

https://github.com/ChristopherGabba/RNV7_TestApp

Screenshots and Videos

Provided above.

Activity

  1. thekiwi commented on Sep 30, 2025

    @thekiwi

    I have been hitting the same RefreshControl tintColor issue after upgrading from RN 0.79.5 to 0.81.4. The RefreshControl always adopts the appropriate color for the device's color scheme and ignores any tintColor I set. I've also managed to recreate it in a minimal vanilla project and am happy to share any additional details if that would help.

  2. zispidd commented on Sep 30, 2025

    @zispidd

    Same issue

  3. kayson-argyle commented on Oct 1, 2025

    @kayson-argyle

    Same here

  4. ChristopherGabba commented on Oct 1, 2025

    @ChristopherGabba
    Author

    Okay so a little update here. I dug into the future 0.82.rc4 react native package and it does seem to resolve the tint color problem.

    It does not seem to fix the "frozen" animation when going back and forth on tabs.

    I spent some time trying to fix that and ended up on this (expand the details):

    Details

    Fully fixed file that you could could copy and use in place of the existing for now:

    File Path:
    /node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm

    /*
     * Copyright (c) Meta Platforms, Inc. and affiliates.
     *
     * This source code is licensed under the MIT license found in the
     * LICENSE file in the root directory of this source tree.
     */
    
    #import "RCTPullToRefreshViewComponentView.h"
    
    #import <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
    #import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
    #import <react/renderer/components/FBReactNativeSpec/Props.h>
    #import <react/renderer/components/FBReactNativeSpec/RCTComponentViewHelpers.h>
    
    #import <React/RCTConversions.h>
    #import <React/RCTRefreshableProtocol.h>
    #import <React/RCTScrollViewComponentView.h>
    
    #import "RCTFabricComponentsPlugins.h"
    
    using namespace facebook::react;
    
    @interface RCTPullToRefreshViewComponentView () <RCTPullToRefreshViewViewProtocol, RCTRefreshableProtocol>
    @end
    
    @implementation RCTPullToRefreshViewComponentView {
      UIRefreshControl *_refreshControl;
      RCTScrollViewComponentView *__weak _scrollViewComponentView;
      BOOL _recycled;
      BOOL _shouldBeRefreshing;
    }
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
      if (self = [super initWithFrame:frame]) {
        self.hidden = YES;
        _props = PullToRefreshViewShadowNode::defaultSharedProps();
        _recycled = NO;
        _shouldBeRefreshing = NO;
        [self _initializeUIRefreshControl];
      }
      return self;
    }
    
    - (void)_initializeUIRefreshControl
    {
      _refreshControl = [UIRefreshControl new];
      [_refreshControl addTarget:self
                          action:@selector(handleUIControlEventValueChanged)
                forControlEvents:UIControlEventValueChanged];
    }
    
    #pragma mark - RCTComponentViewProtocol
    
    + (ComponentDescriptorProvider)componentDescriptorProvider
    {
      return concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>();
    }
    
    - (void)prepareForRecycle
    {
      [super prepareForRecycle];
      _scrollViewComponentView = nil;
      [self _initializeUIRefreshControl];
      _recycled = YES;
    }
    
    - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
    {
      const auto &oldConcreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
      const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
    
      _shouldBeRefreshing = newConcreteProps.refreshing;
    
      if (_recycled || newConcreteProps.tintColor != oldConcreteProps.tintColor) {
        _refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
      }
    
      if (_recycled || newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
        [self _updateProgressViewOffset:newConcreteProps.progressViewOffset];
      }
    
      BOOL needsUpdateTitle = NO;
      if (_recycled || newConcreteProps.title != oldConcreteProps.title) {
        needsUpdateTitle = YES;
      }
      if (_recycled || newConcreteProps.titleColor != oldConcreteProps.titleColor) {
        needsUpdateTitle = YES;
      }
    
      [super updateProps:props oldProps:oldProps];
    
      if (_recycled || needsUpdateTitle) {
        [self _updateTitle];
      }
    
      if (_recycled || newConcreteProps.refreshing != oldConcreteProps.refreshing) {
        if (newConcreteProps.refreshing) {
          [self beginRefreshingProgrammatically];
        } else {
          [_refreshControl endRefreshing];
        }
      }
    
      if (_recycled || newConcreteProps.zIndex != oldConcreteProps.zIndex) {
        _refreshControl.layer.zPosition = newConcreteProps.zIndex.value_or(0);
      }
    
      _recycled = NO;
    }
    
    #pragma mark - Events
    
    - (void)handleUIControlEventValueChanged
    {
      static_cast<const PullToRefreshViewEventEmitter &>(*_eventEmitter).onRefresh({});
    }
    
    - (void)_updateProgressViewOffset:(Float)progressViewOffset
    {
      _refreshControl.bounds = CGRectMake(
          _refreshControl.bounds.origin.x,
          -progressViewOffset,
          _refreshControl.bounds.size.width,
          _refreshControl.bounds.size.height);
    }
    
    - (void)_updateTitle
    {
      const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
      if (concreteProps.title.empty()) {
        _refreshControl.attributedTitle = nil;
        return;
      }
    
      NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
      if (concreteProps.titleColor) {
        attributes[NSForegroundColorAttributeName] = RCTUIColorFromSharedColor(concreteProps.titleColor);
      }
    
      _refreshControl.attributedTitle =
          [[NSAttributedString alloc] initWithString:RCTNSStringFromString(concreteProps.title)
                                          attributes:attributes];
    }
    
    #pragma mark - Attaching & Detaching
    
    - (void)layoutSubviews
    {
      [super layoutSubviews];
    
      if (self.window && _shouldBeRefreshing) {
        // Be cautious here: only recreate if not already refreshing
        if (!(_scrollViewComponentView.scrollView.refreshControl == _refreshControl &&
              _refreshControl.isRefreshing)) {
          [self _recreateAndBeginRefreshing];
        }
      }
    }
    
    - (void)didMoveToWindow
    {
      [super didMoveToWindow];
    
      if (self.window && _shouldBeRefreshing) {
        // Be aggressive here: always recreate on window re-attach
        [self _recreateAndBeginRefreshing];
      }
    }
    
    - (void)didMoveToSuperview
    {
      [super didMoveToSuperview];
      if (self.superview) {
        [self _attach];
      } else {
        [self _detach];
      }
    }
    
    - (void)_attach
    {
      if (_scrollViewComponentView) {
        [self _detach];
      }
    
      _scrollViewComponentView = [RCTScrollViewComponentView findScrollViewComponentViewForView:self];
      if (!_scrollViewComponentView) {
        return;
      }
    
      if (@available(macCatalyst 13.1, *)) {
        _scrollViewComponentView.scrollView.refreshControl = _refreshControl;
        [self setNeedsLayout];
      }
    }
    
    - (void)_detach
    {
      if (!_scrollViewComponentView) {
        return;
      }
    
      [_refreshControl endRefreshing];
    
      if (@available(macCatalyst 13.1, *)) {
        _scrollViewComponentView.scrollView.refreshControl = nil;
      }
      _scrollViewComponentView = nil;
    }
    
    #pragma mark - Refresh control helpers
    
    - (void)beginRefreshingProgrammatically
    {
      _shouldBeRefreshing = YES;
      [self _ensureSpinnerIsAnimating];
    }
    
    - (void)_ensureSpinnerIsAnimating
    {
      if (!_scrollViewComponentView) {
        return;
      }
    
      UIScrollView *scrollView = _scrollViewComponentView.scrollView;
      if (!scrollView) {
        return;
      }
    
      if (_shouldBeRefreshing) {
        if (scrollView.refreshControl == _refreshControl && _refreshControl.isRefreshing) {
          return; // Already animating fine
        }
        [self _recreateAndBeginRefreshing];
      }
    }
    
    - (void)_recreateAndBeginRefreshing
    {
      if (!_scrollViewComponentView) {
        return;
      }
    
      UIScrollView *scrollView = _scrollViewComponentView.scrollView;
      if (!scrollView) {
        return;
      }
    
      [self _initializeUIRefreshControl];
      
      // Apply all properties BEFORE assigning to scrollView.refreshControl
      const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
      _refreshControl.tintColor = RCTUIColorFromSharedColor(concreteProps.tintColor);
      _refreshControl.layer.zPosition = concreteProps.zIndex.value_or(0);
      [self _updateTitle];
      [self _updateProgressViewOffset:concreteProps.progressViewOffset];
      
      // Now assign to scrollView
      scrollView.refreshControl = _refreshControl;
    
      // Only adjust offset if not already pulled enough
      CGFloat currentOffsetY = scrollView.contentOffset.y;
      CGFloat refreshHeight = _refreshControl.frame.size.height;
      if (currentOffsetY > -refreshHeight) {
        CGPoint offset = {scrollView.contentOffset.x,
                          currentOffsetY - refreshHeight};
        [scrollView setContentOffset:offset animated:NO];
      }
    
      [_refreshControl beginRefreshing];
    }
    
    #pragma mark - Native commands
    
    - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
    {
      RCTPullToRefreshViewHandleCommand(self, commandName, args);
    }
    
    - (void)setNativeRefreshing:(BOOL)refreshing
    {
      _shouldBeRefreshing = refreshing;
      if (refreshing) {
        [self beginRefreshingProgrammatically];
      } else {
        [_refreshControl endRefreshing];
      }
    }
    
    #pragma mark - RCTRefreshableProtocol
    
    - (void)setRefreshing:(BOOL)refreshing
    {
      [self setNativeRefreshing:refreshing];
    }
    
    #pragma mark -
    
    - (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
    {
      return @"RefreshControl";
    }
    
    @end
    
    Class<RCTComponentViewProtocol> RCTPullToRefreshViewCls(void)
    {
      return RCTPullToRefreshViewComponentView.class;
    }
    

    Here is the combined diff for react-native 0.81.4:

    diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
    index 0d231bc..53e8285 100644
    --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
    +++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
    @@ -24,23 +24,21 @@ using namespace facebook::react;
     @end
     
     @implementation RCTPullToRefreshViewComponentView {
    -  BOOL _isBeforeInitialLayout;
       UIRefreshControl *_refreshControl;
       RCTScrollViewComponentView *__weak _scrollViewComponentView;
    +  BOOL _recycled;
    +  BOOL _shouldBeRefreshing;
     }
     
     - (instancetype)initWithFrame:(CGRect)frame
     {
       if (self = [super initWithFrame:frame]) {
    -    // This view is not designed to be visible, it only serves UIViewController-like purpose managing
    -    // attaching and detaching of a pull-to-refresh view to a scroll view.
    -    // The pull-to-refresh view is not a subview of this view.
         self.hidden = YES;
    -
    -    _isBeforeInitialLayout = YES;
    +    _props = PullToRefreshViewShadowNode::defaultSharedProps();
    +    _recycled = NO;
    +    _shouldBeRefreshing = NO;
         [self _initializeUIRefreshControl];
       }
    -
       return self;
     }
     
    @@ -63,57 +61,55 @@ using namespace facebook::react;
     {
       [super prepareForRecycle];
       _scrollViewComponentView = nil;
    -  _props = nil;
    -  _isBeforeInitialLayout = YES;
       [self _initializeUIRefreshControl];
    +  _recycled = YES;
     }
     
     - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
     {
    -  // Prop updates are ignored by _refreshControl until after the initial layout, so just store them in _props until then
    -  if (_isBeforeInitialLayout) {
    -    _props = std::static_pointer_cast<const PullToRefreshViewProps>(props);
    -    return;
    -  }
    -
       const auto &oldConcreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
       const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
     
    -  if (newConcreteProps.tintColor != oldConcreteProps.tintColor) {
    +  _shouldBeRefreshing = newConcreteProps.refreshing;
    +
    +  if (_recycled || newConcreteProps.tintColor != oldConcreteProps.tintColor) {
         _refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
       }
     
    -  if (newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
    +  if (_recycled || newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
         [self _updateProgressViewOffset:newConcreteProps.progressViewOffset];
       }
     
       BOOL needsUpdateTitle = NO;
    -
    -  if (newConcreteProps.title != oldConcreteProps.title) {
    +  if (_recycled || newConcreteProps.title != oldConcreteProps.title) {
         needsUpdateTitle = YES;
       }
    -
    -  if (newConcreteProps.titleColor != oldConcreteProps.titleColor) {
    +  if (_recycled || newConcreteProps.titleColor != oldConcreteProps.titleColor) {
         needsUpdateTitle = YES;
       }
     
       [super updateProps:props oldProps:oldProps];
     
    -  if (needsUpdateTitle) {
    +  if (_recycled || needsUpdateTitle) {
         [self _updateTitle];
       }
     
    -  // All prop updates must happen above the call to begin refreshing, or else _refreshControl will ignore the updates
    -  if (newConcreteProps.refreshing != oldConcreteProps.refreshing) {
    +  if (_recycled || newConcreteProps.refreshing != oldConcreteProps.refreshing) {
         if (newConcreteProps.refreshing) {
           [self beginRefreshingProgrammatically];
         } else {
           [_refreshControl endRefreshing];
         }
       }
    +
    +  if (_recycled || newConcreteProps.zIndex != oldConcreteProps.zIndex) {
    +    _refreshControl.layer.zPosition = newConcreteProps.zIndex.value_or(0);
    +  }
    +
    +  _recycled = NO;
     }
     
    -#pragma mark -
    +#pragma mark - Events
     
     - (void)handleUIControlEventValueChanged
     {
    @@ -132,7 +128,6 @@ using namespace facebook::react;
     - (void)_updateTitle
     {
       const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
    -
       if (concreteProps.title.empty()) {
         _refreshControl.attributedTitle = nil;
         return;
    @@ -144,7 +139,8 @@ using namespace facebook::react;
       }
     
       _refreshControl.attributedTitle =
    -      [[NSAttributedString alloc] initWithString:RCTNSStringFromString(concreteProps.title) attributes:attributes];
    +      [[NSAttributedString alloc] initWithString:RCTNSStringFromString(concreteProps.title)
    +                                      attributes:attributes];
     }
     
     #pragma mark - Attaching & Detaching
    @@ -153,12 +149,22 @@ using namespace facebook::react;
     {
       [super layoutSubviews];
     
    -  // Attempts to begin refreshing before the initial layout are ignored by _refreshControl. So if the control is
    -  // refreshing when mounted, we need to call beginRefreshing in layoutSubviews or it won't work.
    -  if (_isBeforeInitialLayout) {
    -    _isBeforeInitialLayout = NO;
    +  if (self.window && _shouldBeRefreshing) {
    +    // Be cautious here: only recreate if not already refreshing
    +    if (!(_scrollViewComponentView.scrollView.refreshControl == _refreshControl &&
    +          _refreshControl.isRefreshing)) {
    +      [self _recreateAndBeginRefreshing];
    +    }
    +  }
    +}
     
    -    [self updateProps:_props oldProps:PullToRefreshViewShadowNode::defaultSharedProps()];
    +- (void)didMoveToWindow
    +{
    +  [super didMoveToWindow];
    +
    +  if (self.window && _shouldBeRefreshing) {
    +    // Be aggressive here: always recreate on window re-attach
    +    [self _recreateAndBeginRefreshing];
       }
     }
     
    @@ -185,8 +191,6 @@ using namespace facebook::react;
     
       if (@available(macCatalyst 13.1, *)) {
         _scrollViewComponentView.scrollView.refreshControl = _refreshControl;
    -
    -    // This ensures that layoutSubviews is called. Without this, recycled instances won't refresh on mount
         [self setNeedsLayout];
       }
     }
    @@ -197,7 +201,6 @@ using namespace facebook::react;
         return;
       }
     
    -  // iOS requires to end refreshing before unmounting.
       [_refreshControl endRefreshing];
     
       if (@available(macCatalyst 13.1, *)) {
    @@ -206,17 +209,64 @@ using namespace facebook::react;
       _scrollViewComponentView = nil;
     }
     
    +#pragma mark - Refresh control helpers
    +
     - (void)beginRefreshingProgrammatically
    +{
    +  _shouldBeRefreshing = YES;
    +  [self _ensureSpinnerIsAnimating];
    +}
    +
    +- (void)_ensureSpinnerIsAnimating
     {
       if (!_scrollViewComponentView) {
         return;
       }
     
    -  // When refreshing programmatically (i.e. without pulling down), we must explicitly adjust the ScrollView content
    -  // offset, or else the _refreshControl won't be visible
       UIScrollView *scrollView = _scrollViewComponentView.scrollView;
    -  CGPoint offset = {scrollView.contentOffset.x, scrollView.contentOffset.y - _refreshControl.frame.size.height};
    -  [scrollView setContentOffset:offset];
    +  if (!scrollView) {
    +    return;
    +  }
    +
    +  if (_shouldBeRefreshing) {
    +    if (scrollView.refreshControl == _refreshControl && _refreshControl.isRefreshing) {
    +      return; // Already animating fine
    +    }
    +    [self _recreateAndBeginRefreshing];
    +  }
    +}
    +
    +- (void)_recreateAndBeginRefreshing
    +{
    +  if (!_scrollViewComponentView) {
    +    return;
    +  }
    +
    +  UIScrollView *scrollView = _scrollViewComponentView.scrollView;
    +  if (!scrollView) {
    +    return;
    +  }
    +
    +  [self _initializeUIRefreshControl];
    +  
    +  // Apply all properties BEFORE assigning to scrollView.refreshControl
    +  const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
    +  _refreshControl.tintColor = RCTUIColorFromSharedColor(concreteProps.tintColor);
    +  _refreshControl.layer.zPosition = concreteProps.zIndex.value_or(0);
    +  [self _updateTitle];
    +  [self _updateProgressViewOffset:concreteProps.progressViewOffset];
    +  
    +  // Now assign to scrollView
    +  scrollView.refreshControl = _refreshControl;
    +
    +  // Only adjust offset if not already pulled enough
    +  CGFloat currentOffsetY = scrollView.contentOffset.y;
    +  CGFloat refreshHeight = _refreshControl.frame.size.height;
    +  if (currentOffsetY > -refreshHeight) {
    +    CGPoint offset = {scrollView.contentOffset.x,
    +                      currentOffsetY - refreshHeight};
    +    [scrollView setContentOffset:offset animated:NO];
    +  }
     
       [_refreshControl beginRefreshing];
     }
    @@ -230,6 +280,7 @@ using namespace facebook::react;
     
     - (void)setNativeRefreshing:(BOOL)refreshing
     {
    +  _shouldBeRefreshing = refreshing;
       if (refreshing) {
         [self beginRefreshingProgrammatically];
       } else {
    

    The "hack" is _ensureSpinnerIsAnimating now destroys and recreates the UIRefreshControl when you come back while refreshing is still true. I don't know if this is a great solution to be honest, but it does seem to fix this frozen spinning animation.

    So TLDR: Copying the 0.82.rc4 file and then adding another hack / fix to the animation seems to fix it. Here is the result:

    trim.0D38E934-E152-47AC-AA7F-F0D2E57CE118.MOV

    Note, didn't run any tests anywhere so use at your own risk for now.

    Another note, because RN 0.81 / Expo 54 uses precompiled react-native binaries, patch-package doesn't work on the react-native core package. You have to turn off the precompile like so:

    app.json:
    image

  5. SchroederNathan commented on Oct 1, 2025

    @SchroederNathan

    Still getting this as well

  6. Racha2004 commented on Oct 16, 2025

    @Racha2004

    reproduced the issue.platform:ios 26, component: RefreshControl

  7. survivorsam commented on Oct 16, 2025

    @survivorsam

    Also experiencing this issue

  8. irekrog commented on Oct 27, 2025

    @irekrog

    I used the patch from @ChristopherGabba in 0.81.5 and it works

  9. Jamal-Ud-Din-hub commented on Nov 3, 2025

    @Jamal-Ud-Din-hub

    +1

    "react-native": "0.80.2",

  10. Jamal-Ud-Din-hub commented on Nov 3, 2025

    @Jamal-Ud-Din-hub

    I used the patch from @ChristopherGabba in 0.81.5 and it works

    not working for me "react-native": "0.80.2"

  11. ChristopherGabba commented on Nov 18, 2025

    @ChristopherGabba
    Author

    Note both of these are an issue on iOS 26 and I confirmed on iOS 18 as well.

  12. 2 remaining items

  13. QuangNguyen1607 commented on Dec 13, 2025

    @QuangNguyen1607

    Same here

  14. 0xroylee commented on Dec 16, 2025

    @0xroylee

    same here

  15. sobrinho commented on Jan 2, 2026

    @sobrinho

    Would you try the patch here? #43388 (comment)

  16. FantinRaimbault commented on Jan 24, 2026

    @FantinRaimbault

    same here

  17. luka-matic01 commented on Jan 27, 2026

    @luka-matic01

    Same
    "react-native": "0.81.5"
    IOS

  18. dincozdemir commented on Feb 2, 2026

    @dincozdemir

    This worked for me for the color issue:

                  <RefreshControl
                            {...Platform.OS === 'ios' ? {
                                tintColor: SECONDARY_COLOR,
                            } : {
                                colors: [SECONDARY_COLOR],
                            }}
                            progressBackgroundColor={PRIMARY_COLOR}
                            refreshing={isLoading}
                            onRefresh={loadScreen}
                  />
    
  19. fcformal42 commented on Feb 4, 2026

    @fcformal42

    This worked for me for the color issue:

                  <RefreshControl
                            {...Platform.OS === 'ios' ? {
                                tintColor: SECONDARY_COLOR,
                            } : {
                                colors: [SECONDARY_COLOR],
                            }}
                            progressBackgroundColor={PRIMARY_COLOR}
                            refreshing={isLoading}
                            onRefresh={loadScreen}
                  />
    

    did not workd

  20. LaGregance commented on Feb 6, 2026

    @LaGregance

    Nothing here work for me (RN 0.81.5 & expo 54.0.31).
    Sometime tintColor quickly work while editing the code with hot reload but is randomly back to default color...

  21. cyz1901 commented on Feb 10, 2026

    @cyz1901

    same issue expo 54.0.13 & RN 0.81.5

  22. artymir commented on Feb 11, 2026

    @artymir

    same, RN 0.81.6

  23. pierroo commented on Feb 18, 2026

    @pierroo

    anyone tried the latest RN versions just in case?
    I am not sure this issue is getting any visibility unfortunately, since it MAY sound low priority, when in reality it can have quite an impact if the tintcolor doesn't stand out user doesn't know refresh is happening

  24. JoshWhite commented on Feb 20, 2026

    @JoshWhite

    anyone tried the latest RN versions just in case?

    Just tried with:

    • RN 0.84
    • react: 19.2.3
    • expo 54

    Same issue. Will try the patch next.

  25. nelyousfi commented on Mar 19, 2026

    @nelyousfi

    A hack that works for us till the original issue is fixed:

      const [tintColor, setTintColor] = useState<string | undefined>(
        Platform.select({
          ios: undefined,
          android: refreshColor,
        }),
      );
    
      useEffect(() => {
        if (Platform.OS !== 'ios') {
          return;
        }
        let raf2: number;
    
        const raf1 = requestAnimationFrame(() => {
          raf2 = requestAnimationFrame(() => setTintColor(refreshColor));
        });
    
        return () => {
          cancelAnimationFrame(raf1);
          cancelAnimationFrame(raf2);
        };
      }, [refreshColor]);
  26. davidomid commented on Mar 31, 2026

    @davidomid

    For anyone having problems with the RefreshControl getting stuck on navigation, there was a similar issue reported here and I came up with this solution.

    It should give you similar results to @ChristopherGabba's solution (#53987 (comment)), but using a hook instead of having to dive into the native code / patch anything.

  27. rochagbr commented on Aug 16, 2026

    @rochagbr

    Any update?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions