Repository navigation
馃敟 [馃悰] AttachmentPicker intercepts back events too eagerly聽#3633
Description
Issue
Describe your issue here
I recently upgraded a React Native app to Expo 56, and as part of that refactoring we switched from react-native-pager-view to PagerView from @expo/ui. This new component behaves differently than the prior component, and we quickly noticed that after the change our screens that contained a <Channel> on one of the pages required triggering back on Android twice before the screen would return to the previous screen.
After several hours of debugging we isolated the problem to AttachmentPicker. It contains a back handler that seemingly always intercepts the first back request, (at least when used on a PagerView, I'm not sure yet if that is related or not).
Steps to reproduce
Steps to reproduce the behavior:
- Checkout https://github.com/vanstinator/expo-ui-pagerview-back-bug
- From a machine configured to build and debug React Native on Android,
EXPO_PUBLIC_STREAM_API_KEY=your_stream_key yarn android - Tap the
Open Pagerbutton - Initiate a
backgesture - Observe that the system does not pop your navigation stack and return you to the apps home screen
Expected behavior
When a <Channel> is included anywhere in the view hierarchy and the AttachmentPicker's bottom sheet is not open the back event should never be consumed by Stream's SDK but instead be allowed to propagate up the stack to the parent handler.
Project Related Information
Customization
Click To Expand
Nothing was customized, using purely out-of-the-box components
Offline support
- I have enabled offline support.
- [ ] The feature I'm having does not occur when offline support is disabled. (stripe out if not applicable)
Environment
Click To Expand
I've included a full working reproduction at https://github.com/vanstinator/expo-ui-pagerview-back-bug
package.json:
# N/Areact-native info output:
OUTPUT GOES HERE
- Platform that you're experiencing the issue on:
- iOS
- Android
- iOS but have not tested behavior on Android
- Android but have not tested behavior on iOS
- Both
stream-chat-react-nativeversion you're using that has this issue:9.3.0
- Device/Emulator info:
- I am using a physical device
- OS version:
Android 16 and Android 17 - Device/Emulator:
Pixel 10 Pro Fold
Additional context
The @expo/ui is fully built on top of Jetpack Compose and there have been other issues with event propagation such as expo/expo#46623 so I can't entirely rule out it being something weird there. But I left several breadcrumbs on the repro's README that seem to indicate an actual logic bug in AttachmentPicker https://github.com/vanstinator/expo-ui-pagerview-back-bug/blob/main/README.md
There's also this block that is suspect 馃槄 https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/AttachmentPicker/AttachmentPicker.tsx#L133-L137
Screenshots
screen-20260609-134848-1781027312907.mp4.2.mp4
Click To Expand
Activity
isekovanic commentedon Jun 9, 2026
Hi @vanstinator !
Thanks a bunch for reporting this. I'll let you know when I've had a look !
isekovanic commentedon Jun 10, 2026
Hi again @vanstinator !
So first of all, thank you very much for the very detailed and meticulous bug report ! We always really, really appreciate this 馃
I can confirm that the bug is indeed reproducible on your repro and I believe I've found a fix.
@gorhom/bottom-sheet'sanimatedIndexisn't a state machine, it's auseDerivedValuethat linearly interpolatesanimatedPositionbetweendetents[0]andclosedDetentPositionto a[0, -1](in our SDK specifically) range, with CLAMP extrapolation. Both anchors are functions ofcontainerHeight(set by the host container'sonLayout):detents[0] = max(0, containerHeight - 333)andclosedDetentPosition = containerHeight.animatedPositionis a separateSharedValueonly moved bysetToPosition()/animateToPosition().Derived values recompute synchronously the instant
containerHeightchanges, but the position correction path is reactive and runs a tick later:So, when
containerHeightchanges fromh1toh2, the detents move to the new domain immediately butanimatedPositionis still sitting ath1. In that one tick gap, ifcontainerHeightgrew by at least the picker height (333px), the stuck position falls below the newdetents[0]and clamp snapsanimatedIndexto exactly0. The next tick anchors the position and the index goes back to-1(so visually we see nothing, it all happens on the UI thread), but by thenAttachmentPickerhas already scheduledrunOnJS(reactToIndex)(0)and writtenselectedPicker = 'images'into the JS store.So why does
Composetrigger it and standard RN doesn't ?Here's where it gets a bit tricky. In standard RN,
Yogacomputes all frames in one pass and dispatches a singleonLayout.containerHeightgoes from-999to its final value in one step, the race window never opens. Compose'sSubcomposeLayouthowever (which@expo/ui'sPagerViewuses for offscreen pages) doesn't do that, but rather it composes with placeholder dimensions, then remeasures when the page becomes visible, dispatchingonLayoutto the RN tree each time. Every transition is a chance to satisfy h2 >= h1 + 333.Two SDK details make it worse.
pickerTopInsetis computed fromwindowHeight(and we need this so accessibility works), so whenComposepublishes a different effective window during composition the host container grows by exactly the picker height. AndsetToPositionhas an asymmetricif (!targetPosition) return;guard, so whilecontainerHeightis briefly0(offscreenComposecomposing),animatedPositionnever gets anchored at all and any later flicker has free reign.This PR however, should resolve it. The
reactToIndexfilter assumesanimatedIndex === 0only ever happens as the destination of a user initiated animation, never as a transient interpolation sideeffect.Composeviolates that on every mount.currentIndexRefon the other hand, is only written viaonAnimate, which only fires fromanimateToPosition(), which only runs for an actual snap to a snap point. The transient skips that path entirely, so the ref cleanly separates "the user opened the sheet" from "the index flickered through 0 during layout".fix: expo ui bottom sheet synchronization issues (#3636)
isekovanic commentedon Jun 10, 2026
This has now been released in
9.3.1!Changelog: https://github.com/GetStream/stream-chat-react-native/releases/tag/v9.3.1
vanstinator commentedon Jun 10, 2026
Wow thanks for the great writeup, and the fast fix!