Skip to content

Conversation

Copy link

Copilot AI commented Nov 16, 2025

Summary

Fixes #702 - Memory leaks when component is disposed

When PagerView is unmounted, delegates and callbacks retain references to the component, preventing deallocation. This causes memory to accumulate proportionally to the resources held by active pages.

iOS (RNCPagerViewComponentView.mm)

  • Clear UIPageViewController and UIScrollView delegates in dealloc, willMoveToSuperview, and prepareForRecycle
  • Release view controllers array in prepareForRecycle

Android

  • Store OnPageChangeCallback reference in NestedScrollableHost for cleanup
  • Unregister callback and clear adapter in onDropViewInstance (PagerViewViewManager)
  • Remove views from containers in onViewRecycled (ViewPagerAdapter)

Project Configuration

  • Removed accidentally added package-lock.json file (project uses Bun, not npm)
  • Added package-lock.json to .gitignore to prevent future additions

Test Plan

What's required for testing (prerequisites)?

  • React Native app with PagerView
  • Memory profiler (Xcode Instruments for iOS, Android Studio Profiler for Android)

What are the steps to reproduce (after prerequisites)?

  1. Mount PagerView with large resources (e.g., high-resolution images)
  2. Unmount the component (navigate away or toggle visibility)
  3. Repeat steps 1-2 multiple times
  4. Observe memory usage remains stable instead of accumulating

Compatibility

OS Implemented
iOS
Android

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I updated the typed files (TS and Flow)

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.


Note

Cleans up PagerView callbacks, delegates, and adapters on unmount (iOS/Android) and recycles holder views; adds package-lock.json to .gitignore.

  • Android:
    • Store OnPageChangeCallback in NestedScrollableHost and unregister it in onDropViewInstance.
    • Clear ViewPager2 adapter in onDropViewInstance to release child references.
    • Remove child views in ViewPagerAdapter.onViewRecycled.
  • iOS:
    • Clear UIPageViewController and UIScrollView delegates in dealloc, willMoveToSuperview, and prepareForRecycle.
    • Reset scrollView and empty _nativeChildrenViewControllers during recycle.
  • Config:
    • Add package-lock.json to .gitignore.

Written by Cursor Bugbot for commit 71650e3. This will update automatically on new commits. Configure here.

Copilot AI and others added 2 commits November 16, 2025 09:56
Co-authored-by: troZee <12766071+troZee@users.noreply.github.com>
Co-authored-by: troZee <12766071+troZee@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix memory leaks when component is disposed Fix memory leaks when PagerView is unmounted Nov 16, 2025
Copilot AI requested a review from troZee November 16, 2025 10:02
Copilot finished work on behalf of troZee November 16, 2025 10:02
@efstathiosntonas
Copy link

this addresses only the Fabric, it should also address the Paper.

@troZee
Copy link
Collaborator

troZee commented Nov 16, 2025

Hello @efstathiosntonas
From version 7.x, only the new architecture is supported.

https://github.com/callstack/react-native-pager-view?tab=readme-ov-file#migration

@efstathiosntonas
Copy link

efstathiosntonas commented Nov 16, 2025

@troZee understood but this patch would suffice:

  diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  index eacfbe8..c61745f 100644
  --- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  +++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  @@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
           _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
           _coalescingKey = 0;
           _eventDispatcher = eventDispatcher;
  -        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
  +        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
           _overdrag = NO;
           _layoutDirection = @"ltr";
       }

It would be nice if it could be squeezed 😅

@troZee
Copy link
Collaborator

troZee commented Nov 16, 2025

@copilot
Could you also apply and test the following patch

  diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  index eacfbe8..c61745f 100644
  --- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  +++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  @@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
           _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
           _coalescingKey = 0;
           _eventDispatcher = eventDispatcher;
  -        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
  +        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
           _overdrag = NO;
           _layoutDirection = @"ltr";
       }

Copy link
Author

Copilot AI commented Nov 16, 2025

@copilot
Could you also apply and test the following patch

...

The file ReactNativePageView.m doesn't exist in the current codebase. Version 7.x only supports the new architecture (Fabric), so there's no Paper architecture implementation to patch.

The equivalent memory leak fix for the new architecture has already been applied in commit 4bb3c51 to RNCPagerViewComponentView.mm, where delegates are properly cleared in dealloc, willMoveToSuperview, and prepareForRecycle.

If you need support for the old Paper architecture (versions < 7.x), this would need to be applied to a different branch that still contains ReactNativePageView.m.

Copilot finished work on behalf of troZee November 16, 2025 10:18
@efstathiosntonas
Copy link

efstathiosntonas commented Nov 16, 2025

thanks @troZee !

I just runned it on ios and on a Pixel7a A16 and no issues so far. I use this as a photo carousel on posts so usage is extremely heavy per render on the list. So far so good.

using Paper.

leaving the full patch here just in case another poor soul is still using Paper:

react-native-pager-view+6.8.1.patch

Click me to reveal patch
diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  index 87b58d0f..e9d0ace1 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  @@ -25,6 +25,7 @@ class NestedScrollableHost : FrameLayout {
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
     public var initialIndex: Int? = null
     public var didSetInitialIndex = false
  +  public var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
     private var touchSlop = 0
     private var initialX = 0f
     private var initialY = 0f
  diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  index 8ec286a7..19f46363 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  @@ -52,7 +52,7 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
           vp.isSaveEnabled = false

           vp.post {
  -            vp.registerOnPageChangeCallback(object : OnPageChangeCallback() {
  +            val callback = object : OnPageChangeCallback() {
                   override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                       super.onPageScrolled(position, positionOffset, positionOffsetPixels)
                       UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
  @@ -79,7 +79,9 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
                               PageScrollStateChangedEvent(host.id, pageScrollState)
                       )
                   }
  -            })
  +            }
  +            host.pageChangeCallback = callback
  +            vp.registerOnPageChangeCallback(callback)
               UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
                       PageSelectedEvent(host.id, vp.currentItem)
               )
  @@ -200,6 +202,20 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
           }
       }

  +    override fun onDropViewInstance(view: NestedScrollableHost) {
  +        // Unregister the page change callback to prevent memory leaks
  +        val viewPager = PagerViewViewManagerImpl.getViewPager(view)
  +        view.pageChangeCallback?.let { callback ->
  +            viewPager.unregisterOnPageChangeCallback(callback)
  +            view.pageChangeCallback = null
  +        }
  +
  +        // Clear the adapter to release references to child views
  +        viewPager.adapter = null
  +
  +        super.onDropViewInstance(view)
  +    }
  +
       override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Map<String, String>> {
           return MapBuilder.of(
                   PageScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageScroll"),
  diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  index fd3530e1..8aab5f66 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  @@ -30,6 +30,12 @@ class ViewPagerAdapter() : Adapter<ViewPagerViewHolder>() {
       container.addView(child)
     }

  +  override fun onViewRecycled(holder: ViewPagerViewHolder) {
  +    super.onViewRecycled(holder)
  +    // Clean up the holder's container to prevent memory leaks
  +    holder.container.removeAllViews()
  +  }
  +
     override fun getItemCount(): Int {
       return childrenViews.size
     }
  diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  index eacfbe8..c61745f 100644
  --- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  +++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  @@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
           _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
           _coalescingKey = 0;
           _eventDispatcher = eventDispatcher;
  -        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
  +        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
           _overdrag = NO;
           _layoutDirection = @"ltr";
       }

Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot, please remove this lock file. This project uses Bun instead of npm

Copy link
Author

Choose a reason for hiding this comment

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

Removed in commit 71650e3. Also added package-lock.json to .gitignore to prevent it from being added in the future.

Co-authored-by: troZee <12766071+troZee@users.noreply.github.com>
Copilot AI requested a review from troZee November 16, 2025 10:28
Copilot finished work on behalf of troZee November 16, 2025 10:28
@troZee troZee marked this pull request as ready for review November 16, 2025 10:30
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Bug: Nil object access during component recycling.

After prepareForRecycle sets scrollView = nil on line 158, updateProps accesses scrollView.scrollEnabled without a nil check. If the component is recycled and updateProps is called before willMoveToSuperview reinitializes the view, this causes a crash or undefined behavior when accessing properties on a nil object.

ios/RNCPagerViewComponentView.mm#L204-L206

if (newScreenProps.scrollEnabled != scrollView.scrollEnabled) {
scrollView.scrollEnabled = newScreenProps.scrollEnabled;

Fix in Cursor Fix in Web


Bug: Imperative Commands Crash on Nil State Access

The setScrollEnabledImperatively method accesses scrollView without checking if it's nil. After prepareForRecycle sets scrollView = nil on line 158, calling this imperative command from JavaScript will crash or produce undefined behavior. Since this is an imperative API callable at any time, it needs a nil check.

ios/RNCPagerViewComponentView.mm#L425-L428

- (void)setScrollEnabledImperatively:(BOOL)scrollEnabled {
[scrollView setScrollEnabled:scrollEnabled];
}

Fix in Cursor Fix in Web


})
}
host.pageChangeCallback = callback
vp.registerOnPageChangeCallback(callback)
Copy link

Choose a reason for hiding this comment

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

Bug: Race Condition Causes Memory Leak

The callback is registered inside vp.post {}, deferring execution to the next frame, but onDropViewInstance may be called before this posted runnable executes. This creates a race condition where the callback gets registered after the view is dropped, causing a memory leak since it's never unregistered. The callback should be stored and registered synchronously, or the cleanup should account for pending posted runnables.

Fix in Cursor Fix in Web


// Clear view controllers array
[_nativeChildrenViewControllers removeAllObjects];

Copy link

Choose a reason for hiding this comment

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

Bug: Lifecycle issue crashes keyboard dismissal.

The shouldDismissKeyboard method accesses scrollView.keyboardDismissMode without checking if scrollView is nil. After prepareForRecycle sets scrollView = nil, calling this method (e.g., from updateProps on line 202) will crash or produce undefined behavior.

Fix in Cursor Fix in Web

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory Leaks when component is disposed

3 participants