Skip to content

Commit c79b2d0

Browse files
authored
Merge pull request Expensify#88923 from callstack-internal/fix/keep-scrollanchor-mounted
fix(iOS): keep ScrollAnchor mounted to prevent scroll reset when MVCP toggles
2 parents e1ef619 + 1810dd5 commit c79b2d0

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js
2+
index ee42f63..4e8d8c0 100644
3+
--- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js
4+
+++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js
5+
@@ -380,16 +380,15 @@ const RecyclerViewComponent = (props, ref) => {
6+
}
7+
return onScrollHandler;
8+
}, [onScrollHandler, scrollY, stickyHeaders, stickyHeaderUseNativeDriver]);
9+
- const shouldMaintainVisibleContentPosition = recyclerViewManager.shouldMaintainVisibleContentPosition();
10+
const maintainVisibleContentPositionInternal = useMemo(() => {
11+
- if (shouldMaintainVisibleContentPosition) {
12+
+ if (maintainVisibleContentPosition != null) {
13+
return {
14+
...maintainVisibleContentPosition,
15+
minIndexForVisible: 0,
16+
};
17+
}
18+
return undefined;
19+
- }, [maintainVisibleContentPosition, shouldMaintainVisibleContentPosition]);
20+
+ }, [maintainVisibleContentPosition]);
21+
const shouldRenderFromBottom = recyclerViewManager.getDataLength() > 0 &&
22+
((_d = maintainVisibleContentPosition === null || maintainVisibleContentPosition === void 0 ? void 0 : maintainVisibleContentPosition.startRenderingFromBottom) !== null && _d !== void 0 ? _d : false);
23+
// Create view for measuring bounded size
24+
@@ -401,11 +399,11 @@ const RecyclerViewComponent = (props, ref) => {
25+
}, ref: firstChildViewRef }));
26+
}, [horizontal, stickyHeaderOffset]);
27+
const scrollAnchor = useMemo(() => {
28+
- if (shouldMaintainVisibleContentPosition) {
29+
+ if (maintainVisibleContentPosition != null) {
30+
return (React.createElement(ScrollAnchor, { horizontal: Boolean(horizontal), scrollAnchorRef: scrollAnchorRef }));
31+
}
32+
return null;
33+
- }, [horizontal, shouldMaintainVisibleContentPosition]);
34+
+ }, [horizontal, maintainVisibleContentPosition]);
35+
// console.log("render", recyclerViewManager.getRenderStack());
36+
// Render the main RecyclerView structure
37+
return (React.createElement(RecyclerViewContextProvider, { value: recyclerViewContext },
38+
diff --git a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx
39+
index b2bd67a..d4bf02d 100644
40+
--- a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx
41+
+++ b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx
42+
@@ -572,18 +572,15 @@ const RecyclerViewComponent = <T,>(
43+
return onScrollHandler;
44+
}, [onScrollHandler, scrollY, stickyHeaders, stickyHeaderUseNativeDriver]);
45+
46+
- const shouldMaintainVisibleContentPosition =
47+
- recyclerViewManager.shouldMaintainVisibleContentPosition();
48+
-
49+
const maintainVisibleContentPositionInternal = useMemo(() => {
50+
- if (shouldMaintainVisibleContentPosition) {
51+
+ if (maintainVisibleContentPosition != null) {
52+
return {
53+
...maintainVisibleContentPosition,
54+
minIndexForVisible: 0,
55+
};
56+
}
57+
return undefined;
58+
- }, [maintainVisibleContentPosition, shouldMaintainVisibleContentPosition]);
59+
+ }, [maintainVisibleContentPosition]);
60+
61+
const shouldRenderFromBottom =
62+
recyclerViewManager.getDataLength() > 0 &&
63+
@@ -604,7 +600,7 @@ const RecyclerViewComponent = <T,>(
64+
}, [horizontal, stickyHeaderOffset]);
65+
66+
const scrollAnchor = useMemo(() => {
67+
- if (shouldMaintainVisibleContentPosition) {
68+
+ if (maintainVisibleContentPosition != null) {
69+
return (
70+
<ScrollAnchor
71+
horizontal={Boolean(horizontal)}
72+
@@ -613,7 +609,7 @@ const RecyclerViewComponent = <T,>(
73+
);
74+
}
75+
return null;
76+
- }, [horizontal, shouldMaintainVisibleContentPosition]);
77+
+ }, [horizontal, maintainVisibleContentPosition]);
78+
79+
// console.log("render", recyclerViewManager.getRenderStack());
80+

patches/@shopify/flash-list/details.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@
4848
- Upstream PR/issue: TBD
4949
- E/App issue: https://github.com/Expensify/App/issues/33725
5050
- PR introducing patch: https://github.com/Expensify/App/pull/85114
51+
52+
### [@shopify+flash-list+2.3.0+007+fix-scroll-anchor-unmount-on-ios.patch](@shopify+flash-list+2.3.0+007+fix-scroll-anchor-unmount-on-ios.patch)
53+
54+
- Reason: Fixes a scroll position reset on iOS when `maintainVisibleContentPosition.disabled` toggles from `true` to `false` (e.g. when `shouldMaintainVisibleContentPosition` changes based on scroll offset). Root cause: `ScrollAnchor` was conditionally rendered based on `shouldMaintainVisibleContentPosition()`. When MVCP was disabled, the anchor unmounted, which made the native Fabric `_firstVisibleView` weak-ref become nil. When MVCP was re-enabled, the anchor remounted at `top: 1,000,000` (its initial position), but `_prevFirstVisibleFrame` was stale at `1,000,000 + X` from the prior anchor instance. `_adjustForMaintainVisibleContentPosition` then computed `deltaY = 0 - (1,000,000 + X)` — a massive negative offset — causing the list to jump to the start. The fix decouples anchor lifetime from the `disabled` flag: `ScrollAnchor` is now always mounted (and `maintainVisibleContentPositionInternal` always non-null) whenever `maintainVisibleContentPosition` prop is defined. The `disabled` flag continues to gate JS-level `scrollBy` corrections in `applyOffsetCorrection` (via `shouldMaintainVisibleContentPosition()`), so the anchor stays in place when MVCP is logically off — the native side always has a live `_firstVisibleView` and a fresh `_prevFirstVisibleFrame` to diff against.
55+
- Files changed: Both `src/recyclerview/RecyclerView.tsx` and `dist/recyclerview/RecyclerView.js`.
56+
- Upstream PR/issue: TBD
57+
- E/App issue: https://github.com/Expensify/App/issues/33725
58+
- PR introducing patch: TBD

0 commit comments

Comments
 (0)