Patrick Michalik

“React Native is native” and other careful statements

I should say up front that I came to React Native the long way around: I spent several years building native Android apps before working professionally with React Native for a year and change. I mention this not as a credential but to explain my angle: When you arrive at a cross-platform framework already knowing what the platform underneath can do, you may notice different things than someone coming from the web. It may also explain why the Android grievances below are the most heartfelt.

I’ve shipped things with React Native that worked and that people used, and I bear it no personal ill will. It’s possible to hold all of that in one hand and, in the other, the settled conviction that the framework is built on a small architectural fib that it has spent a decade paying for. This note is about the fib.

React Native puts real platform views on the screen, then interposes its own models and schedulers across much of what governs them—event handling, animation, layout, touch, text entry, scrolling, and navigation. Every interposition leaks a little.

“React Native is native” is the framework’s most famous sentence, and it’s true in the way that a museum diorama contains real taxidermy. The views are genuinely native;1 much of what decides where they go, when they move, and what happens when you touch them is an elaborate reconstruction. The seams between the reconstruction and the real thing are where React Native developers spend their most memorable evenings.

One thread, many jobs

React Native runs your state updates, business logic, and event handlers in one JavaScript runtime; React’s render phase and layout ordinarily run there too. When you raise this, you’ll be told that “the UI runs on the UI thread.” This is one of those statements that’s true in a way that requires careful listening. The UI thread can keep drawing the native view hierarchy already mounted. The work that produces its next state—which views it contains, how they’re configured and laid out, and what changes in response to input—normally begins on the JavaScript thread, behind any application JavaScript already running. React can interrupt and reprioritize its own render work; it can’t preempt an event handler or other application JavaScript already in progress.2

The result is a distinctive failure mode. A congested JavaScript thread needn’t disturb native scrolling; it can make the app serenely ignore you.3 The interface scrolls at a beautiful 120 frames per second while your taps accomplish nothing, list cells arrive late, and navigation pauses thoughtfully after you press the button.

Web-application logic often faces a similar main-thread constraint, but the web has a compensating advantage: thirty years of browser engineering devoted to keeping scrolling and rendering responsive when JavaScript is busy. React Native inherited the constraint, left much of that machinery behind, and put the result in users’ hands beside apps built directly for the platform.

The standard response to performance complaints is that the developer was doing it wrong—and in any individual case, that may even be so. But when the same problems recur across unrelated teams and unrelated codebases—and they do, with the reliability of a train timetable—“doing it wrong” stops being an explanation and starts being a description of the default path. A framework where staying fast requires continuous expert vigilance has, whatever else you want to call it, a performance problem.

The animation situation

The ecosystem’s celebrated answer to janky animations is Reanimated, a library whose central idea is to install a second JavaScript runtime on the UI thread.4 I’d encourage you to sit with that sentence for a moment, because it’s an architectural confession wearing a party hat.

Reanimated is a genuine achievement, and its authors are clearly brilliant. It’s also less free than it looks. Setting it up isn’t free: Animated styles and gestures have to be workletized and connected to views.5 Running it isn’t free either: Active animations execute JavaScript in that UI runtime. A second runtime also means a second heap and garbage collector.6 Here, that garbage-collected runtime performs frame-critical animation calculations mostly on the UI thread. Layout-property animations also recalculate layout on every frame.7

The comparison point is worth keeping in mind. For standard property and layer animations, Android’s ObjectAnimator updates the target property itself, while iOS’s Core Animation performs the frame-by-frame drawing after the application supplies the animation’s parameters. Neither requires a per-frame application callback.8,9 The gap between describing an animation once and running application logic every frame is the gap you can feel with your thumb.

Credit where due: In 2026, React Native announced an experimental animation backend that lets the native driver animate layout properties such as width and height.10 This is good news, and I mean that sincerely. It is also true that the native driver learned to animate a view’s dimensions roughly nine years after React Native introduced the useNativeDriver option with the caveat that layout properties were unsupported.11 Some milestones tell you more about the journey than the destination.

Android pays a toll

The New Architecture’s headline—synchronous native access via JSI—is quietly iOS-shaped. Objective-C++ lets C++ meet iOS APIs directly, while Android modules implemented against Kotlin or Java still cross JNI.12 Both platforms lose the old serialized bridge; Android often retains another toll booth.

You can see the team knows this, because the New Architecture includes MapBuffer—a compact data structure designed specifically to make Java–C++ communication through JNI faster.13 They retired the JavaScript bridge, then designed a new format to talk efficiently to their own platform. The bridge is dead; long live the bridge.

Meanwhile, the Android host layer remains built on the classic view system, which Google has placed in maintenance mode.14 Jetpack Compose remains absent from core. Predictive-back support is narrower than the name suggests: The framework’s own guidance promises animation only when exiting an app, not when navigating within it.15

The parallel universe

React Native maintains its own authoritative model of where everything is—the shadow tree, laid out by Yoga, its own Flexbox engine. The platform’s layout system isn’t authoritative.16 Everywhere the two models can disagree becomes a category of bug that’s now yours to own.

Touch shows what this means. Android hit testing walks the real view hierarchy; Fabric’s coordinate APIs consult the shadow tree. Ordinary React Native transforms keep the two accounts in agreement, but native-owned placement has to update both. Bottom sheets, React Native’s Modal, and React Native Screens all carry code to reconcile them.17 Leave either ledger stale, and pixels, coordinates, and touch targets part company. One interface, two sets of books.

Then there’s my favorite single line in the codebase: ReactViewGroup.requestLayout() is a no-op.18 There are sound internal reasons—Android’s layout machinery would quarrel with Yoga—but requestLayout isn’t a suggestion box; it’s the documented protocol by which a view announces that its size is stale, and the Android view ecosystem is written against it. Embed any native child, and its perfectly correct requests for layout can stop at its React parent. The remedy—driving measure and layout from a frame callback—has become folklore-level canonical: a small ritual repeated across wrappers.

Text inputs, or the distributed-systems keyboard

Formatting input as the user types—for currency amounts, payment-card numbers, or phone numbers—is, natively, a solved and slightly boring problem. Native text-change hooks let the correction happen synchronously inside the edit cycle.19,20 It can’t flicker, structurally, any more than a sentence can arrive before you’ve said it.

React Native can turn this into a distributed-systems problem. The native field displays your keystroke; JavaScript hears about it through onChangeText; a controlled, formatted value then travels back. Two stateful parties exchange messages about a shared value under live human load. The raw character can flash before the mask catches it, and cursor position becomes another piece of state to synchronize. React Native’s own TextInput documentation warns that forcing the controlled native value can cause flicker.21

The community’s accumulated wisdom is to format on blur, use refs, or move the masking to native code so the correction never crosses the boundary. This amounts to advice not to use the framework’s flagship idiom—UI as a function of state—for text entry with transformations. Which is a modest thing to give up if you don’t count forms.

The compensating machinery

There’s a pattern worth naming: When a React Native primitive struggles with its one job, a third-party package appears to do the job “more natively.” Individually, these are success stories; collectively, they’re a syllabus.

Lists are virtualized through React rendering, so quick scrolling can outrun the logic that fills the cells. React Native’s VirtualizedList documentation explicitly warns that asynchronous offscreen rendering can lose the race with the fill rate and produce the signature blank-cell shimmer.22 The platforms’ own list controls had recycled cells since their earliest releases; the React Native ecosystem eventually had to reintroduce the idea in Shopify’s FlashList,23 which advertises, with unusual directness, “no more blank cells.”

Scroll events are throttled on their way into JavaScript; scrollEventThrottle makes the price of hearing about every frame part of the API.24 Navigation commonly gets React Native Screens25 to hand screens back to native fragments and view controllers. A third-party package to make navigation native is a striking thing for a framework with that particular slogan to require.

Keyboard handling lives in React Native Keyboard Controller.26 Gestures live in React Native Gesture Handler.27 Images, for years, lived in FastImage.28

Lists, scroll events, navigation, keyboard handling, gestures, images. At some point, one is entitled to ask what remains of the defaults.

Expo and the vanishing native project

React Native’s native-project template changes enough between releases that maintaining a hand-edited native project through upgrades became a well-known misery. Expo’s response—continuous native generation29—wasn’t to stabilize the template but to declare the native project build output. In this workflow, the android and ios folders are added to .gitignore and regenerated from configuration when needed.

The machinery required is remarkable. Config plugins are JavaScript functions that mutate a parsed model of your native project. Expo’s configuration APIs expose Android manifests as JSON, Gradle and native source files as strings, and the Xcode project through a JavaScript parser for the pbxproj format—an OpenStep-era property-list dialect.30 For anything these APIs cannot express, the official escape hatch gives plugins raw access to project files and is named, with admirable candor, “dangerous mods.” The documentation explicitly discusses string manipulation and regular expressions.31 The full stack can thus end in JavaScript using regular expressions to patch native code and running a parser to rewrite build metadata—all to produce a native project that will, in the end, run JavaScript.

I find continuous native generation genuinely fascinating, the way one is fascinated by an elaborate bridge built around a mountain that could have been tunneled. The underlying idea—generated projects as build artifacts—is respectable, but notice where the complexity lands: React Native’s generated projects need an entire plugin programming model because the framework’s native footprint is enormous and most serious apps end up extending or configuring it. The sophistication of the machinery is proportional to the instability it exists to absorb. “React Native is native, and the native part is best treated as untouchable generated code” is a sentence whose two halves have clearly never been introduced to each other.

Assorted other exhibits

  • Builds: A clean React Native build can still spend geological time compiling a template-heavy C++ core and the native surface of its dependencies. React Native has begun shipping parts of that stack precompiled,32 but the problem is far from solved.
  • Startup: Load the bundle, boot the JavaScript engine, execute the framework, run the module graph, and then render React content. Hermes’s ahead-of-time bytecode removes compilation work from startup, but not the rest of that sequence.33 No bytecode format digs beneath that floor.
  • Tooling: An app split across two runtimes has a seam running through its diagnostics: A crash arrives bearing a JavaScript stack trace or a native one, each ends politely at the border, and no tool sees the whole program. The debugger, meanwhile, keeps being redesigned: Chrome remote debugging was deprecated; Flipper arrived, became the default, and was retired; the current answer is React Native DevTools.34 This is roughly what one would expect of a window asked to look into two rooms at once.
  • The edges of the platform: The platform no longer ends at the main app window—Android widgets; iOS widgets, Live Activities, and App Intents. These are system-owned surfaces and integrations, defined declaratively in advance. The founding trick—real views, steered at runtime from JavaScript—has nothing to steer. The ecosystem’s answer arrived in 2026, when Expo Widgets brought iOS widgets and Live Activities into React.35 It does so by translating a deliberately restricted React dialect into SwiftUI in a separate runtime. It covers only those two iOS cases; Android widgets and general App Intents still need separate paths.36 “React Native is native” turns out to mean the app window. Past its edge, the instinct repeats: Insert another model between the developer and the platform, then document where it stops.

On the discourse

React Native’s online reputation runs well ahead of the experience above, and I don’t think anything sinister is required to explain it. The loudest advocates are web developers for whom the framework’s promise is personally, genuinely true. The comparison in their lives isn’t “React Native versus a finely made native app” but “React Native versus never shipping a phone app at all”—and by that measure, it’s a marvel. Add a platform vendor with a stake, businesses whose fortunes ride on the framework being chosen, and a content economy in which React Native material has a much larger potential audience than Kotlin and Swift material, and you can get disproportionate enthusiasm without a single conspirator. Meanwhile, the disappointments are structurally silent: Few people write the post titled “We shipped something adequate, and our users can’t tell why it feels slightly off.”

Two rhetorical habits are worth noticing all the same. The first is the sliding definition, in which “React Native is native” retreats, under questioning, to “React Native renders real platform views”—true—while continuing to imply “Your app will feel native,” which does not follow from it. The second is the perpetual fresh start, in which criticism is met with the news that it applied only to the old architecture. This defense has accompanied the move from the bridge to JSI, Fabric, and bridgeless mode.37 A framework may reinvent itself as often as it likes, but when every generation’s problems are attributed to the previous generation, the pattern eventually becomes data about the family.

The short version

React Native puts real native views on screen but interposes its own systems across much of what governs them. Those systems leak, the ecosystem’s proudest packages patch the leaks, and Expo’s response is to regenerate the native project because the native layer proved too lively for human maintenance.

None of this makes a good product impossible; plenty of skilled people ship one, and the team you have may make React Native the sensible business choice. But when the aim is the best possible app rather than the best available trade-off, building with React Native becomes a continuous act of skilled resistance against one’s own foundation—and life offers more rewarding things to resist.

References

  1. Core components and native components,” React Native documentation 

  2. Threading model,” React Native documentation 

  3. Performance overview,” React Native documentation 

  4. Runtime kinds,” React Native Worklets documentation 

  5. Worklets,” React Native Reanimated documentation 

  6. Sharing memory,” React Native Worklets documentation 

  7. Performance,” React Native Reanimated documentation 

  8. Property-animation overview,” Android Developers documentation 

  9. Core Animation basics,” Core Animation Programming Guide, Apple Developer Documentation Archive 

  10. React Native 0.85: New animation backend, new Jest preset package,” React Native blog 

  11. Using native driver for Animated,” React Native blog 

  12. Using Codegen,” React Native documentation 

  13. MapBuffer: Data structure optimized for JNI access,” React Native New Architecture Working Group, GitHub 

  14. Android UI development is Compose first,” Android Developers’ Blog 

  15. Android 16 changes impacting React Native,” React Native: Discussions and Proposals, GitHub 

  16. Render, commit, and mount,” React Native documentation 

  17. Fabric-integration findings,” react-native-bottom-sheet, GitHub 

  18. ReactViewGroup.kt, lines 240–244, React Native 0.86.0 source, GitHub 

  19. InputFilter, Android Developers reference 

  20. textField(_:shouldChangeCharactersIn:replacementString:), Apple Developer Documentation 

  21. TextInput, React Native documentation 

  22. VirtualizedList, React Native documentation 

  23. FlashList documentation 

  24. ScrollView, React Native documentation 

  25. React Native Screens documentation 

  26. React Native Keyboard Controller documentation 

  27. React Native Gesture Handler documentation 

  28. react-native-fast-image, GitHub 

  29. Continuous native generation,” Expo documentation 

  30. PropertyListSerialization.PropertyListFormat.openStep, Apple Developer Documentation 

  31. Mods,” Expo documentation 

  32. React Native 0.84: Hermes V1 by default,” React Native blog 

  33. Hermes as the default,” React Native blog 

  34. React Native DevTools,” React Native documentation 

  35. iOS widgets and Live Activities are stable in Expo SDK 56,” Expo blog 

  36. Widgets,” Expo documentation 

  37. New Architecture is here,” React Native blog 

© 2026 Patrick Michalik