Skip to content

feat(Reanimated): play CSS transitions and layout animations on the native host on Android - #10951

Draft
pawicao wants to merge 1 commit into
@pawicao/native-animations--hand-over-and-test-hooksfrom
@pawicao/native-animations--android
Draft

pawicao wants to merge 1 commit into
@pawicao/native-animations--hand-over-and-test-hooksfrom
@pawicao/native-animations--android

Conversation

@pawicao

@pawicao pawicao commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Note

This pull request was authored by AI on behalf of @pawicao.

Summary

The shared native animation host played only on iOS, and Android CSS transitions used their own backend (CSSPlatformTransitions, CSSPlatformEasings, and the Kotlin css package). I moved the CSS transition backend of the host to common code (CSSHostTransitionBackend), with a route table from each platform, and added the Android platform: AndroidNativeAnimationPlatform calls the Kotlin NativeAnimationExecutor, which plays each track with one ValueAnimator. With ANDROID_CSS_PLATFORM_TRANSITIONS on, Android CSS transitions now run through the host, and the new static flag ANDROID_NATIVE_LAYOUT_ANIMATIONS (default false) lets the host play opacity, position, and 2D transform tracks of layout animations from the final mounted state, on the same conditions as on iOS. The layout route needs the React Native flags enableMountingCoordinatorPullModelAndroid and enableAccumulatedUpdatesInRawPropsAndroid, so that the pull and the mount of a commit happen in one dispatch on the UI thread; without them, layout animations stay on the frame driver. With the flags off, Android runs as before, and the feature flags guide lists the new flag and its limits.

How it works

Common CSS backend. The CSS backend of the host was Apple code. It is now CSSHostTransitionBackend in common code, and each platform gives a route table as data: the CSS properties that it plays, their targets, and the timing kinds that it plays. REACSSPlatformRouteTable keeps the 8 properties and the 2 timing kinds of iOS. AndroidCSSRouteTable gives opacity, the background and border colors, the border radius, and the shadow color from API 28. AnimationTiming got StepsTiming and LinearStopsTiming, and only Android plays them. iOS refuses them, so such transitions stay on the CSS loop there.

Android executor. AndroidNativeAnimationPlatform sends the tracks through JNI to the Kotlin NativeAnimationExecutor (package nativeAnimations). The executor plays each track with one ValueAnimator and one writer for its target, and starts each track on its origin on the Reanimated clock. Opacity plays by alpha. Position and transform play by View.setAnimationMatrix, which needs API 29 or later. A skew, a singular matrix, a 3D operation, and a change of width or height stay on the frame driver.

NativeProxy now builds PlatformDepMethodsHolder one time. Before, it built it two times, so the CSS proxy and the run-end listener used different backends. One behavior changed: the host does not wait for a view that is not mounted. It refuses the run, and CSS plays the property on its loop.

Mount window. The iOS route starts native playback in the mount report of the pull (runAfterMount). On Android, the Fabric UIManagerListener of NativeProxy gives willMountItems and didMountItems to the platform, which numbers each dispatch. At the start of each pull, the proxy asks mountReportOfPull for the number of the dispatch that mounts it. A pull with no number (outside a dispatch, or in the push model) admits no native start. The proxy keeps a PullRecord for each pull until a report takes it. didMountItems reports all pulled surfaces (pulledSurfacesDidMount) and then rewrites the synchronous props. pullsOnMountThread() replaces isMountingCoordinatorPullModelEnabled() at each site, and it needs both React Native flags.

On Android, React Native writes opacity to the view property that the track also writes. When a mount changes the opacity of a view with an active opacity track, the pull records a MountedModel, and the host gives the new model value to the platform (modelDidChange). iOS does nothing there, because the layer keeps its model value during a track.

Test hooks and test host. The Gradle property reanimatedRuntimeTestHooks sets REANIMATED_RUNTIME_TEST_HOOKS in the Android CMake build, and the runtime-tests driver passes it. A new hook, nativeAnimationRoutes, gives the routes of the build (layoutRoute is off, configured, or admitted), so each suite skips by what the build has. The Android test host got modules for a second surface, a view sampler, slow animations, and a view manager that throws. The Gradle property fabricExampleMountThreadPull turns on the two React Native flags in fabric-example.

flowchart TD
  Commit["React commit"] --> Dispatch["Fabric dispatch: willMountItems"]
  Dispatch --> Pull["LayoutAnimationsProxy pull"]
  Pull --> Report{"mountReportOfPull has a number?"}
  Report -->|no| FD["frame driver"]
  Report -->|yes| Record["PullRecord: MountedStart / MountedStop / MountedModel"]
  Record --> Mount["mount items run"]
  Mount --> Did["didMountItems: pulledSurfacesDidMount(dispatch)"]
  Did --> Host["NativeAnimationHost::runAfterMount"]
  Host --> Platform["AndroidNativeAnimationPlatform"]
  Platform --> Executor["Kotlin NativeAnimationExecutor: one ValueAnimator per track"]
  CSS["CSS transition"] --> Backend["CSSHostTransitionBackend + AndroidCSSRouteTable"]
  Backend --> Host
Loading

Test plan

Jest: packages/react-native-reanimated/__tests__/nativeTiming.android.test.ts (new: only the Android flag gives a native timing description on Android).

Runtime tests:

  • The new suite native layout mount window (apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutMountWindow.test.tsx, Android) checks that a start mounts its final state and is admitted in the didMountItems of its dispatch, and that a pull outside a dispatch admits no native start.
  • The suite css native transitions (apps/common-app/runtime-tests/reanimated/tests/css/nativeTransitions.test.tsx) adds the Android cases: a clipped view, the steps and linear() easings of the route table, and the commands of a second surface.
  • The suite native layout route adds the cases of a build whose route has no mount-thread pull.
  • The other native layout suites read the Android samples.

CI runs the runtime tests on Android in DebugRuntimeTests, so the native test hooks are there. The CI build does not set fabricExampleMountThreadPull, so the layout route is configured but admits no native start. On Android CI, these suites run: css native transitions (the host cases, also of a second surface), native layout route (the hooks, the route reads, and the cases with no mount-thread pull), and layout animation callback commit (on the frame driver). The suites native layout starts, native layout mount window, native layout entering, native layout exiting, native layout continuity, and native layout transform skip there.

To run the native layout suites on Android, build the app with the pull model, then run the suites on the build:

cd apps/fabric-example/android && ./gradlew assembleDebugRuntimeTests -PfabricExampleMountThreadPull=true -PreactNativeArchitectures=<abi>
yarn workspace fabric-example runtime-tests --library reanimated --platform android --serial <serial> --skip-build --only 'native layout route,native layout starts,native layout mount window,native layout entering,native layout exiting,native layout continuity'

Changelog

  • I added a changelog fragment with yarn changelog:add for each changed package, or this PR does not change react-native-reanimated or react-native-worklets.

…ative host on Android

Bring the shared native animation host to Android, for CSS transitions and for layout animations.
The CSS transition backend of the host moves from Apple code to `CSSHostTransitionBackend` in common
code. Each platform gives a route table as data: the CSS properties that it plays, their targets,
and the timing kinds that it plays (`REACSSPlatformRouteTable`, `AndroidCSSRouteTable`). The iOS
table has the same 8 properties and 2 timing kinds as before. `AnimationTiming` gets `StepsTiming`
and `LinearStopsTiming`. iOS refuses them, so such transitions stay on the CSS loop there.

CSS transitions: with `ANDROID_CSS_PLATFORM_TRANSITIONS`, Android transitions now run through the
host. `AndroidNativeAnimationPlatform` calls the Kotlin `NativeAnimationExecutor` through JNI. The
executor plays each track with one `ValueAnimator` and one writer for its target, and starts each
track on its origin on the Reanimated clock. Remove the old backend: `CSSPlatformTransitions`,
`CSSPlatformEasings`, and the Kotlin `css` package. `NativeProxy` now builds
`PlatformDepMethodsHolder` one time. Before, it built it two times, so the CSS proxy and the run-end
listener used different backends. One behavior changes: the host does not wait for a view that is
not mounted. It refuses the run, and CSS plays the property on its loop.

Layout animations: the new static flag `ANDROID_NATIVE_LAYOUT_ANIMATIONS` (default `false`) lets
platform animators play opacity, position, and 2D transform tracks from the final mounted state, on
the same conditions as on iOS. The route needs the React Native pull model
(`enableMountingCoordinatorPullModelAndroid` and `enableAccumulatedUpdatesInRawPropsAndroid`), so
the route choice and the mount are in one frame. `pullsOnMountThread()` gates each site, and a pull
outside a mount dispatch admits no native start. `didMountItems` reports the pulled surfaces
(`pulledSurfacesDidMount`) and then rewrites the synchronous props. Opacity plays by alpha. Position
and transform play by `View.setAnimationMatrix` on API 29 or later. A skew, a singular matrix, a 3D
operation, and a change of width or height stay on the frame driver.

With the flags off, Android runs as before. In the push model, the React Native default, layout
animations stay on the frame driver. The feature flags guide lists the new flag and its limits.

Tests: `packages/react-native-reanimated/__tests__/nativeTiming.android.test.ts`.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The pull request adds Android native layout animations and moves Android CSS transitions to the shared native animation host. It adds mount-report handling, Android animation playback and geometry support, and platform route configuration. It also adds Android runtime-test modules and expands CSS transition and layout-animation test coverage across platforms.

Suggested labels: lib: Reanimated

Priority: ➖ Normal

Change: Feature

Merge Risk: 🔵 Low · up to 3f7ef

This change moves Android CSS transitions to the shared native host and adds an opt-in native layout animation path. The remaining issue is limited to the runtime-test app: if React Native's internal fields change, a reflection failure crashes the test host instead of failing the test cleanly. Production behavior is not affected, and the issue can be fixed before or shortly after merge.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: Android support for CSS transitions and layout animations through the native host.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains the Android native animation host, shared CSS backend, native layout animations, runtime test hooks, test coverage, and feature flags changed in the pull…
Full details: Description check

Explanation

The pull request description directly explains the Android native animation host, shared CSS backend, native layout animations, runtime test hooks, test coverage, and feature flags changed in the pull request.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e1446c8e-18c4-49ee-963d-07523d0a1a21
📥 Commits

Reviewing files that changed from the base of the PR and between fafd21c and 3f7eff7.

📒 Files selected for processing: 99
  • apps/common-app/runtime-tests/ReJest/secondSurface.ts
  • apps/common-app/runtime-tests/reanimated/suites.ts
  • apps/common-app/runtime-tests/reanimated/tests/css/nativeTransitions.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/layout/propUpdates.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutContinuity.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutEntering.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutExiting.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutMountWindow.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutReferencePlay.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutRoute.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutStarts.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutTestKit.tsx
  • apps/common-app/runtime-tests/reanimated/tests/layoutAnimations/nativeLayoutTransform.test.tsx
  • apps/common-app/runtime-tests/reanimated/tests/nativeAnimationObservation.ts
  • apps/common-app/runtime-tests/reanimated/tests/viewSampler.ts
  • apps/fabric-example/android/app/build.gradle
  • apps/fabric-example/android/app/src/main/AndroidManifest.xml
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/MainApplication.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsCoverActivity.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsFabricShim.java
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsSlowAnimationsModule.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsSlowAnimationsPackage.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsSurfaceModule.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsSurfacePackage.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsThrowingViewManager.java
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsViewSamplerModule.kt
  • apps/fabric-example/android/app/src/main/java/com/fabricexample/RuntimeTestsViewSamplerPackage.kt
  • apps/fabric-example/package.json
  • apps/fabric-example/scripts/runtime-tests-server.js
  • docs/docs-reanimated/docs/guides/feature-flags.md
  • packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/transition/CSSHostTransitionBackend.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/CSS/core/transition/CSSHostTransitionBackend.h
  • packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/platform.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/platform.h
  • packages/react-native-reanimated/Common/cpp/reanimated/Compat/ReactNativeFeatureFlagsCompat.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/NativeLayoutRoute.h
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationHost.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationHost.h
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationPlatform.h
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationTiming.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationTiming.h
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationValue.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeAnimations/NativeAnimationValue.h
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h
  • packages/react-native-reanimated/__tests__/nativeTiming.android.test.ts
  • packages/react-native-reanimated/android/CMakeLists.txt
  • packages/react-native-reanimated/android/build.gradle.kts
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/AndroidCSSRouteTable.cpp
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/AndroidCSSRouteTable.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformEasings.cpp
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformEasings.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformTransitions.cpp
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformTransitions.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeAnimations/AndroidNativeAnimationPlatform.cpp
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeAnimations/AndroidNativeAnimationPlatform.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeAnimations/JNativeAnimationExecutor.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeAnimations/NativeAnimationPlatformCallbacks.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/OnLoad.cpp
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSEasing.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPropertyWriter.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/MainThreadCommandQueue.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/DurationScale.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/GeometryWriter.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/NativeAnimationExecutor.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/NativeAnimationPlatformCallbacks.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/NativeAnimationSample.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/PreDrawReconciler.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/PreparedPlayback.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/Timeline.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/TrackBinding.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/TrackBufferParser.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/TrackEncoding.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/TrackPlayback.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeAnimations/TrackWriter.kt
  • packages/react-native-reanimated/apple/reanimated/apple/CSS/REACSSPlatformRouteTable.h
  • packages/react-native-reanimated/apple/reanimated/apple/CSS/REACSSPlatformRouteTable.mm
  • packages/react-native-reanimated/apple/reanimated/apple/CSS/REACSSPlatformTransitionBackend.h
  • packages/react-native-reanimated/apple/reanimated/apple/NativeAnimations/REANativeAnimationPlatform.h
  • packages/react-native-reanimated/apple/reanimated/apple/NativeAnimations/REANativeAnimationPlatform.mm
  • packages/react-native-reanimated/apple/reanimated/apple/ReanimatedModule.mm
  • packages/react-native-reanimated/apple/reanimated/apple/native/PlatformDepMethodsHolderImpl.mm
  • packages/react-native-reanimated/changelog/android-css-transitions-shared-native-host.feature.md
  • packages/react-native-reanimated/changelog/android-native-layout-animations.feature.md
  • packages/react-native-reanimated/changelog/native-animation-common-css-backend.other.md
  • packages/react-native-reanimated/changelog/native-animation-runtime-test-hooks.other.md
  • packages/react-native-reanimated/src/animation/nativeTiming.ts
  • packages/react-native-reanimated/src/featureFlags/staticFeatureFlags.ts
  • packages/react-native-reanimated/src/featureFlags/staticFlags.json
💤 Files with no reviewable changes: 10
  • packages/react-native-reanimated/apple/reanimated/apple/CSS/REACSSPlatformTransitionBackend.h
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPropertyWriter.kt
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformTransitions.h
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSEasing.kt
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/MainThreadCommandQueue.kt
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformEasings.cpp
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformEasings.h
  • packages/react-native-reanimated/android/src/main/cpp/reanimated/android/CSS/CSSPlatformTransitions.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/CSS/utils/platform.h

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Comment on lines +216 to +225
fun queuedPullItems(rootTag: Double, promise: Promise) {
UiThreadUtil.runOnUiThread {
val shim = detachedSurfaces[rootTag.toInt()]
if (shim == null) {
promise.reject("E_NO_SURFACE", "No surface with no root view has the root tag $rootTag.")
return@runOnUiThread
}
promise.resolve(shim.queuedPullItemCount())
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reflection failures in queuedPullItemCount crash the test app; reject the promise instead.

RuntimeTestsFabricShim.queuedPullItemCount() reads the private React Native fields mMountingManager and onViewAttachMountItems through reflection. It throws a ReflectiveOperationException or an IllegalStateException if a field is renamed or the surface manager is missing. Kotlin does not force the module to catch these exceptions. At Line 223 and Line 266, the exception escapes the runOnUiThread block. At Line 409, it escapes willMountItems during a mount dispatch. In each case the exception crashes the app, and the promise stays unsettled. The whole runtime-test run then stops with an uncaught exception, and no message names the reflection failure. A React Native upgrade is enough to cause this, because these fields are internal.

Catch the exception at each call site. Reject the pending promise with the cause.

🛡️ Proposed fix
-      promise.resolve(shim.queuedPullItemCount())
+      runCatching { shim.queuedPullItemCount() }
+        .onSuccess { promise.resolve(it) }
+        .onFailure { promise.reject("E_REFLECTION", it) }
   override fun willMountItems(uiManager: UIManager) {
     ...
     if (attach.hasAttached) return
-    attach.queuedPullItemsAtAttach = attach.shim.queuedPullItemCount()
+    attach.queuedPullItemsAtAttach = runCatching { attach.shim.queuedPullItemCount() }.getOrElse {
+      dispatchAttach = null
+      attach.promise.reject("E_REFLECTION", it)
+      return
+    }

Also applies to: 266-266, 404-412

@pawicao
pawicao added this pull request to stack #10315 October 21, 2026 19:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant