appandflow / appandflow/react-native-safe-area-context

SafeAreaProvider never re-emits after a lost insets event — a transient 0 becomes a permanently stale useSafeAreaInsets value (Fabric, Android)

Abierto
#758 0 comentarios 1 reacción 0 asignados Ver en GitHub
Lenguaje dominante
TypeScript
Estrellas
2.8k
Forks
258
Merge medio
3 d 1 h
PR fusionados (30 d)
2

Descripción

### Environment

- react-native-safe-area-context **5.7.0** (issue present by inspection through 5.9.1)
- react-native 0.86.0, **New Architecture** (Fabric + Hermes), edge-to-edge Android
- Reproduced on a physical Samsung SM-A356E, Android 16 (API 36), 3-button navigation

### Summary

This is adjacent to #738 / #364, but reports a different (worse) consequence: when the transient `inset → 0 → inset` sequence those issues describe crosses to JS with the **final corrective event lost**, `useSafeAreaInsets()` does not flicker — it stays wrong **indefinitely** (we measured minutes), while `` on the same screen is correct. The cause is visible in the provider source: recovery after a lost event is structurally impossible.

### What we measured (production app, via React DevTools fiber inspection)

Same app process, no reload between samples:

1. A bottom sheet rendered ~4 minutes after cold start read `insets.bottom === 0` from context (its fallback padding rendered; its last row measured 81px inside the navigation-bar zone via the accessibility tree).
2. The same sheet re-opened ~15 minutes later read `insets.bottom === 48`.
3. Whenever we sampled the `SafeAreaInsetsContext.Provider` fiber's `value` directly, it read 48 — i.e. the JS state eventually healed, but only after some unrelated later inset change; components mounted during the stale window rendered wrong and stayed wrong.
4. With the soft keyboard fully open, the provider still read 48 (keyboard resize ruled out as the trigger on this setup).

### Why one lost event wedges JS permanently (source)

`SafeAreaProvider.kt`:

```kotlin
private fun maybeUpdateInsets() {
val insetsChangeHandler = mInsetsChangeHandler ?: return
val edgeInsets = getSafeAreaInsets(this) ?: return
val frame = getFrame(rootView as ViewGroup, this) ?: return
if (mLastInsets != edgeInsets || mLastFrame != frame) {
insetsChangeHandler(this, edgeInsets, frame) // single event
mLastInsets = edgeInsets // cache updated regardless of delivery
mLastFrame = frame
}
}
```

`mLastInsets` is committed whether or not the emitted event was ever applied on the JS side. `onPreDraw` then recomputes the same (correct) value every frame, compares it to the native cache, and never re-sends. So the sequence

```
emit 48 (applied) → emit 0 (applied) → emit 48 (lost)
```

leaves JS at 0 forever: every subsequent predraw computes 48, finds `mLastInsets == 48`, and stays silent. The dedupe is against the provider's own memory, not against what JS actually received. `` is unaffected because it recomputes per-view on every predraw and pushes through `StateWrapper.updateState` — no JS event hop to lose.

Where the lost/transient emission comes from in the first place is exactly what #738 documents ("SafeAreaProvider emits a transient inset → 0 → inset across separate frames"); on Fabric there are also known windows where events/state updates are dropped (cf. the 5.8.1 fix "skip Fabric SafeAreaView state updates while detached from window").

### Suggested directions

- Only commit `mLastInsets` after delivery is known-good, or
- Re-emit the current value when a JS-side consumer (re)subscribes / on surface mount, or
- Expose a synchronous getter so `initialMetrics`-style reads can re-sync, and/or
- Land #738's deferred-decrease guard, which would suppress the transient 0 at the source (it would not, however, close the general lost-event recovery gap).

### Repro status

No minimal standalone repro yet — the trigger is a nondeterministic startup race (edge-to-edge window-flag churn is our prime suspect; the app also uses react-native-keyboard-controller). Happy to test patches or add instrumentation from the app where we can reproduce the stale window.

### Workaround

Replacing the hook-derived padding at the affected edges with an empty `` spacer fixed it reliably for us — native inset resolution per view, nothing to lose in transit.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.