Architecture
react-native-shared-hero is built specifically for React Native's New Architecture (Fabric) with a thin TypeScript wrapper and a symmetrical native core on iOS and Android.
High-level data flow
┌────────────────────────────────────────────────────────────────────────┐
│ React Native JS (Fabric) │
│ <SharedHero id="photo-1" namespace="default" mode="morph" /> │
└──────────────────────────────────┬─────────────────────────────────────┘
│ Codegen C++ ShadowNode / Props
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Native Endpoint View │
│ iOS: SharedHeroView.mm (ObjC++) -> SharedHeroViewImpl.swift │
│ Android: SharedHeroViewManager.kt -> SharedHeroView.kt │
│ │
│ • Tracks heroId, heroNamespace, mode, timing, borderRadius, bgColor │
│ • Captures HeroSnapshot + live window rect when layout settles │
└──────────────────────────────────┬─────────────────────────────────────┘
│ register / notifyLayout / unregister
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Singleton Registry & Match Engine │
│ iOS: HeroRegistry.shared (HeroRegistry.swift) │
│ Android: HeroRegistry (HeroRegistry.kt) │
│ │
│ • live[key]: ordered list of mounted views per namespace::id │
│ • recentlyUnregistered[key]: parked source snapshots (~300 ms window)│
│ • lastKnownSnapshots: bounded LRU fallback cache │
│ • lastFlightSourceFrame: push/pop symmetry frame hints │
└──────────────────────────────────┬─────────────────────────────────────┘
│ Launches / interrupts flights
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Flight Engine & Window-Level Overlay │
│ iOS: FlightEngine.swift + OverlayHost.swift (UIWindow .alert + 1)│
│ Android: FlightEngine.kt + OverlayHost.kt (ViewGroupOverlay) │
└────────────────────────────────────────────────────────────────────────┘Pairing paths in HeroRegistry
Unlike navigation-coupled libraries that require a transition hook from the router, HeroRegistry pairs endpoints purely from native view lifecycle signals using two trigger paths:
Path 1: Twin appears while another is still live (Stack push, Modal, Sheet)
- Screen A is mounted.
live["default::photo-1"]contains[viewA]. - Screen B mounts
viewBwith the same key.register(viewB)immediately snapshotsviewA(recording its pre-navigation window frame before the stack push slides it) and queues a pending flight intoviewB. - Once
viewBfinishes its Fabric layout pass (notifyLayout) and any zoom-in destination<Image>has decoded (up to a bounded ~120 ms wait),HeroRegistryhands both snapshots and geometries toFlightEngine.
Path 2: Unregister / Return flight & In-place state swap
- Back pop / Modal dismiss: When
viewBdetaches from its window whileviewAis still live (or re-attaching underneath),unregister(viewB)capturesviewB's current live window frame (including any drag-to-dismiss transform) and flies back toviewA's resting frame (destFrameHint). - In-place toggle: When
viewAunmounts andlive[key]becomes empty,unregister(viewA)parksviewA's snapshot inrecentlyUnregistered[key](with a300 msmatching window andsourceViewIdchurn guard). WhenviewBmounts with the same key on the next tick,runMatchPasspairs the parkedPendingSourcewithviewBand runs the flight.
Platform implementation details
iOS (ios/*.swift + SharedHeroView.mm)
OverlayHost.swift: Lazily creates a transparent, non-interactiveUIWindow(windowLevel = .alert + 1,isUserInteractionEnabled = false) backed byOverlayRootViewController. The window stays visible once prepared so flights never suffer a 1-frame render-server creation flash, and defers status bar / orientation queries to the app's real root window.FlightEngine.swift: Drives time-based flights withUIViewPropertyAnimator(UICubicTimingParameters) and spring flights withUISpringTimingParameters(mass:stiffness:damping:initialVelocity:). Supports mid-air takeover (canTakeOver/interrupt) by readingflightView.layer.presentation()geometry.InteractiveStackPop.swift&InteractiveModalReturn.swift: Attach aCADisplayLinkobserver toUINavigationController'sinteractivePopGestureRecognizerand UIKit modal/sheet swipe dismissals so the overlay tracks finger progress 1:1 and accounts for underlying list parallax.
Android (android/src/main/java/com/sharedhero/*.kt)
OverlayHost.kt: ResolvesViewGroupOverlayfromview.rootView(DecorView). Because it inspects the destination view's attachedrootViewfirst and falls back toactivity.window.decorView.overlay, flights work both inside the main Activity window and across React Native<Modal>Dialog windows.FlightEngine.kt: Renders a customFrameLayoutcontainer in theViewGroupOverlaywith a dynamicViewOutlineProvider(outline.setRoundRect(0, 0, w, h, currentRadius)) formorphmode. Runs cubic-Bezier flights viaChoreographer+PathInterpolatorand spring flights viaandroidx.dynamicanimation.animation.SpringAnimation(FloatValueHolder(0f..1000f)).HeroRegistry.kt&HeroSnapshot.kt: Maintain a boundedlastKnownSnapshotsLRU cache so nativeBitmapallocations stay strictly bounded even after opening hundreds of detail screens.