Fix memory leaks when PagerView is unmounted
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, andprepareForRecycle - 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.jsonfile (project uses Bun, not npm) - Added
package-lock.jsonto.gitignoreto 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)?
- Mount PagerView with large resources (e.g., high-resolution images)
- Unmount the component (navigate away or toggle visibility)
- Repeat steps 1-2 multiple times
- 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
OnPageChangeCallbackinNestedScrollableHostand unregister it inonDropViewInstance.- Clear
ViewPager2adapter inonDropViewInstanceto release child references.- Remove child views in
ViewPagerAdapter.onViewRecycled.- iOS:
- Clear
UIPageViewControllerandUIScrollViewdelegates indealloc,willMoveToSuperview, andprepareForRecycle.- Reset
scrollViewand empty_nativeChildrenViewControllersduring recycle.- Config:
- Add
package-lock.jsonto.gitignore.Written by Cursor Bugbot for commit 71650e3c79e8dda31eb86c849925621fc0c5854b. This will update automatically on new commits. Configure here.
this addresses only the Fabric, it should also address the Paper.
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
@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 😅
@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";
}
@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.
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";
}