callstack / callstack/react-native-brownfield
[Android] ReactNativeFragment leaks OnBackPressedCallback, breaking back after re-entry
- Dominant language
- TypeScript
- Stars
- 547
- Forks
- 52
- Avg merge
- 3d 16h
- Merged PRs (30d)
- 5
Description
### Summary
`ReactNativeBrownfield.createView` adds an `OnBackPressedCallback` without a `LifecycleOwner` and never removes it. `ReactNativeFragment.onCreateView` calls `createView` for each new view, so callbacks from destroyed views remain enabled for the lifetime of the activity.
In a single-activity host, back works on the first visit to the React Native screen. After leaving and reopening it, hardware/gesture back stops working.
### Environment
| | |
| --- | --- |
| `@callstack/react-native-brownfield` | 5.1.0 |
| React Native | 0.87.0 |
| Android | API 35 emulator |
| Host | Single activity; `ReactNativeFragment` is directly on the activity fragment back stack |
### Minimal reproduction
[findoo/react-native-brownfield-back-press-repro](https://github.com/findoo/react-native-brownfield-back-press-repro)
The reproduction reduces this repository's Android example to one `AppCompatActivity`, one native fragment, one `ReactNativeFragment`, and a React Native component containing only `Text`. It has no JavaScript navigation stack or `BackHandler`.
1. Tap **Open React Native**.
2. Press hardware or gesture back. The native fragment returns.
3. Tap **Open React Native** again.
4. Press back again.
Expected: the native fragment returns.
Actual: the React Native fragment remains visible. Further visits add further callbacks, and back remains broken until the activity is recreated.
https://github.com/user-attachments/assets/7c60a9af-38c7-47d1-8c03-6275d89842ac
### Cause
[`createView`](https://github.com/callstack/react-native-brownfield/blob/bfd88af993733450c7f3a74d829f81c63120fac1/packages/react-native-brownfield/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt#L200-L234) registers the callback using the non-lifecycle-aware overload:
```kotlin
activity?.onBackPressedDispatcher?.addCallback(backPressedCallback)
```
That overload requires explicit removal, but no removal exists. [`ReactNativeFragment.onCreateView`](https://github.com/callstack/react-native-brownfield/blob/bfd88af993733450c7f3a74d829f81c63120fac1/packages/react-native-brownfield/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt#L49-L59) calls `createView` again for every view creation.
When the active callback disables itself to pass an unhandled press to the dispatcher, a callback retained from the previous view receives it instead. The dispatcher therefore never reaches the host fragment manager.
The lifecycle observer at lines 232–233 does not cover this: `ReactNativeFragment` supplies a delegate, so that branch is skipped, and the observer does not remove the callback in any case.
### Expected behaviour
Callbacks belonging to destroyed fragment views should not receive back presses. Back should behave the same on every visit.
### Related
- #227 fixes a callback not being re-enabled after forwarding a back press. This issue concerns callbacks from destroyed views not being removed.
- #354 concerns multiple React Navigation stacks on iOS. This reproduction is Android-only and has no JavaScript navigation stack.
Contributor guide
Assessment
This issue has not been assessed yet.