From 3811747ef6ffc1533656da8744c3f8cea1d40d38 Mon Sep 17 00:00:00 2001 From: Audrius Savickas Date: Fri, 12 Jun 2026 18:08:03 +0300 Subject: [PATCH] fix(Reanimated): guard preserveMountedTags against resolveView throwing (#9649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #9636. `NativeProxy.preserveMountedTags` assumes `FabricUIManager.resolveView` returns `null` for a tag whose view isn't mounted: ```kotlin for (i in tags.indices) { if (mFabricUIManager.resolveView(tags[i]) == null) { tags[i] = -1 } } ``` In practice `resolveView` (→ `SurfaceMountingManager.getView`) **throws `IllegalViewOperationException`** when the tag's `ViewState` is already registered but the Android view object hasn't been created yet. That uncaught throw on the main thread is fatal. This is reachable when a view is **mid-preallocation** and a third-party view manager dispatches an event **synchronously from inside `createView`**, re-entering Reanimated's event path before the view exists. `lottie-react-native` does exactly this: when a composition is already in Lottie's in-memory cache, `setAnimation` fires `onAnimationLoaded` synchronously during `createViewInstance`, which dispatches through `FabricEventDispatcher` → `NodesManager.onEventDispatch` → `performOperations` → `preserveMountedTags` → `resolveView` on the tag that is still being preallocated. Observed crash: ``` com.facebook.react.uimanager.IllegalViewOperationException: Unable to find view for tag N. Surface 1 stopped: false, rootViewAttached: true at com.facebook.react.fabric.mounting.SurfaceMountingManager.getView at com.facebook.react.fabric.FabricUIManager.resolveView at com.swmansion.reanimated.NativeProxy.preserveMountedTags at com.swmansion.reanimated.NativeProxy.performOperations (Native Method) ... at com.airbnb.android.react.lottie.LottieAnimationViewManagerImpl.sendAnimationLoadedEvent at com.airbnb.lottie.LottieAnimationView.setComposition ... at com.facebook.react.fabric.mounting.SurfaceMountingManager.createViewUnsafe at com.facebook.react.fabric.mounting.SurfaceMountingManager.preallocateView ``` The fix wraps the `resolveView` call in a try/catch and treats `IllegalViewOperationException` the same as the existing `null` branch — the tag has no usable view yet, so mark it `-1`. This matches the intent of the original null check (a tag without a mounted view is dropped) and is the minimal hot-fix that eliminates the crash. As @bartlomiejbloniarz noted in the issue, this path can eventually be removed once the pull model lands on Android. Minimal standalone Expo repro (crashes within ~1s of launch on Android, New Architecture, before this change; no crash with it applied): **https://github.com/audrius-savickas/lottie-reanimated-9636-repro** ```bash npm install npx expo run:android ``` It mounts several `Animated.createAnimatedComponent(LottieView)` instances with `entering`/`exiting` and remounts them every 250 ms, with the Lottie source bundled locally so the first remount is a guaranteed cache hit (the synchronous `onAnimationLoaded` during preallocation). Full captured trace is in [`CRASH-STACKTRACE.txt`](https://github.com/audrius-savickas/lottie-reanimated-9636-repro/blob/main/CRASH-STACKTRACE.txt). Versions matching the production crash: `react-native@0.85.3` (New Arch), `react-native-reanimated@4.3.1`, `react-native-worklets@0.8.3`, `lottie-react-native@7.3.4`. Co-authored-by: audrius.sav Co-authored-by: Claude Opus 4.8 (cherry picked from commit 502cf702717639e2023f312f09bba4e3657a2d03) --- .../com/swmansion/reanimated/NativeProxy.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.java index ff87a3a83e23..d2313751eb34 100644 --- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.java +++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.java @@ -15,6 +15,7 @@ import com.facebook.react.common.annotations.FrameworkAPI; import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; +import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.common.UIManagerType; import com.facebook.soloader.SoLoader; @@ -305,7 +306,19 @@ public boolean preserveMountedTags(int[] tags) { } for (int i = 0; i < tags.length; i++) { - if (mFabricUIManager.resolveView(tags[i]) == null) { + try { + if (mFabricUIManager.resolveView(tags[i]) == null) { + tags[i] = -1; + } + } catch (IllegalViewOperationException e) { + // `resolveView` is expected to return `null` for a tag without a + // mounted view, but it instead throws when the tag's `ViewState` is + // already registered while the Android view hasn't been created yet. + // This happens when a view is mid-preallocation and a third-party view + // manager (e.g. lottie-react-native) dispatches an event synchronously + // from within `createView`, re-entering this code path. Treat it the + // same as a missing view. + // See https://github.com/software-mansion/react-native-reanimated/issues/9636. tags[i] = -1; } }