Commit cac5e2d
fix: don't emit contentOffset {0,0} on the first animatedProps evaluation in ScrollViewWithBottomPadding (#1496)
## 📃 Description
`ScrollViewWithBottomPadding`'s `useAnimatedProps` emits `contentOffset
= {x: 0, y: 0}` on its **first** evaluation: `prevContentOffsetY` is
initialized to `null`, the current `contentOffsetY.value` is `0`, and `0
!== null` passes the change guard.
On Fabric, the initial animatedProps values merge over the wrapped
component's own props. So when the wrapped ScrollView is given a
`contentOffset` prop — e.g. a virtualized chat list mounting with an
initial "scroll at end" offset (this is exactly what `@legendapp/list`'s
`KeyboardAwareLegendList` integration does via
`renderScrollComponent={KeyboardChatScrollView}`) — that prop is
silently overridden by `{x:0, y:0}` and the list mounts scrolled to the
top natively, while the list's JS model believes it's at its initial
offset. The list is then at the mercy of its own watchdog/retry logic,
whose re-dispatched scroll commands race the (also asynchronous)
animated `contentInset` commit and can land short (RN's Fabric
`scrollTo` command clamps against the *native* `contentInset` at
execution time).
We hit this in production as an intermittent "conversation opens with
the last message hidden behind the composer" bug on RN 0.85 / Reanimated
4 / New Arch. Full write-up of the investigation (with on-device
measurements of the native inset via scroll-event
`nativeEvent.contentInset`): the at-rest position depended on the
ordering between Reanimated's shadow-tree commit of the inset and the
list's initial scroll dispatch.
## Fix
Swallow the initial evaluation: record the first value without emitting
`contentOffset`, and only emit on subsequent changes.
The shift mechanism is unaffected:
- `contentOffsetY` is a fresh shared value starting at `0` for every
mount, so the swallowed first value is always the meaningless `{0,0}`;
- any real shift (keyboard `onStart`, `useExtraContentPadding` reaction)
writes a new value, which is emitted exactly as before — including a
legitimate later shift *to* `0`, since by then
`prevContentOffsetY.value` is no longer `null`.
## 🧐 What to verify
- Chat example: keyboard open/close shifts, `extraContentPadding`
changes, and interactive dismiss still behave identically (the first
*real* shift is still emitted).
- A wrapped ScrollView with a non-zero `contentOffset` prop now actually
mounts at that offset on Fabric.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: kirillzyusko <zyusko.kirik@gmail.com>1 parent b7b47e3 commit cac5e2d
1 file changed
Lines changed: 7 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
152 | 156 | | |
153 | 157 | | |
| 158 | + | |
| 159 | + | |
154 | 160 | | |
155 | 161 | | |
156 | 162 | | |
| |||
0 commit comments