callstack / callstack/react-native-brownfield

[Android] ReactNativeFragment leaks OnBackPressedCallback, breaking back after re-entry

Open
#457 1 comment 0 reactions 1 assignee Claimed by @likevy View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.