Skip to content

Android Fabric can drop loadComplete because TopChangeEvent is coalesced with pageChanged #1009

@mavrickdeveloper

Description

@mavrickdeveloper

Hi folks so we are facing an issue at Expensify with this lib , On Android/Fabric, react-native-pdf can emit both loadComplete and pageChanged natively, but JS sometimes only receives pageChanged.

This happens because both callbacks are routed through the same TopChangeEvent / topChange event path, and TopChangeEvent inherits React Native's default coalescing behavior.

In practice, the later pageChanged event can replace the earlier loadComplete event for the same PDF view before JS receives it.

We hit this in Expensify while validating local PDF receipts on Android:

Video demo

Video.Project.15.mov

Affected code

  • android/src/main/java/org/wonday/pdf/PdfView.java
    • onPageChanged(...) dispatches pageChanged|... through TopChangeEvent
    • loadComplete(...) dispatches loadComplete|... through TopChangeEvent
  • android/src/main/java/org/wonday/pdf/events/TopChangeEvent.java
    • always returns the same event name, topChange
    • does not override canCoalesce()
  • fabric/RNPDFPdfNativeComponent.js
    • exposes a single bubbling event prop, onChange

Expected behavior

If native emits both loadComplete and pageChanged, JS should receive both callbacks in order.

Actual behavior

Native Android finishes loading the PDF, but JS can miss onLoadComplete entirely because the event was coalesced away before delivery.

Why this matters

This breaks the onLoadComplete contract under Android/Fabric.

In our Expensify case, it showed up during PDF validation: native rendered the PDF successfully, but JS never received onLoadComplete, so the app treated the PDF as if validation had not completed.

Root cause

loadComplete and pageChanged are currently treated as the same coalescible event type for the same view:

  • same native event class: TopChangeEvent
  • same event name: topChange
  • same Fabric event prop: onChange
  • default canCoalesce() == true

That allows Fabric to keep only the later event.

Proposed fix

Make TopChangeEvent non-coalescible on Android:

@Override
public boolean canCoalesce() {
    return false;
}

This is the smallest fix that preserves the existing event model while ensuring both loadComplete and pageChanged reach JS.

Alternative

A larger alternative would be to split these callbacks into distinct native/Fabric events instead of multiplexing them through shared topChange, but that seems unnecessary for fixing the event loss itself.

Validation

i confirmed the behavior locally with temporary native/JS tracing:

  • before state:
    • native emitted loadComplete
    • native emitted pageChanged
    • JS only received pageChanged
  • after disabling coalescing:
    • JS received loadComplete
    • JS then received pageChanged

That points to event coalescing as the source of the loss.

Patch PR

#1011

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions