One Year Into React Native: Where Shopify Still Writes Native Code
Last year Shopify committed its mobile developer resources to React Native. A year later, the decision still holds, but not without forcing some hard rethinks about mobile best practices, re-standardized patterns, and which open-source projects to adopt or support. Most importantly, it forced a clear policy on when—and how—to reach for iOS and Android native features while keeping the React Native layer as the primary UI and logic surface.
Shopify's Point of Sale app became the testbed for this policy, and it shaped the company’s approach to native code across its React Native apps.
A "Native Only When Necessary" Starting Point
For a developer coming from native iOS or Android, writing platform code is the norm. In React Native, it’s the exception—something you take on after rereading the Native Modules documentation and usually with some reluctance. Initially, Shopify steered strongly against writing Native UI components or Native Modules, adopting an attitude of “only when absolutely necessary.”
As a guiding principle during the team’s early exploration of React Native’s darker corners, that stance served well. If you only ever make the best decisions, the reasoning goes, you never see why some decisions are better than others.
Where the Policy Broke Down
Two pain points exposed the limits of staying purely in the JavaScript (JS) layer. First, certain UI components that combine scrolling, dragging, dropping, and high performance are easy to churn on. A particularly telling example is a grid of buttons that can be long-pressed (like the iOS home screen) to become draggable and reorderable. The grid can hold hundreds of buttons in a scroll view, with tile-type-specific designs. The gesture and animation complexity made the team question whether they were asking too much of React Native—and whether such views should be Native UI Components.
Second, background processes proved frustrating. To support offline mode, the app routinely syncs its local database with the server. As background jobs multiplied, responsiveness suffered. The single-threaded JS and UI threads were bottlenecked even though the heavy lifting (network and database) was delegated to native libraries that were assumed to be well behaved—that is, non-blocking. They were not.
The JS job handler was a list of services enqueuing small job functions, with the entire system running once a minute on a timer. Short gaps between jobs were meant to free the main thread for interactions, but the overhead of bridge crossings during heavy native-module work made the app nearly unresponsive whenever jobs ran.
The takeaway: fully committing to React Native meant also committing to native-code solutions where they actually applied.
Keep UI in React Native, Move Jobs Out
Surprisingly, Shopify decided not to pull complex views into native. Instead, they leaned further into React Native. Maintaining separate UI implementations for Android and iOS was seen as a worse tradeoff than solving the hard gesture-view problem in a shared codebase. Using why-did-you-render, the React devtools profiler, and an internal Application Performance Index measuring tool, the team improved rendering performance and adopted Reanimated 2 for drag-and-drop. Most gains came from reducing unnecessary re-renders and deferring render work. Memoizing at runtime and precomputing ahead of time made the biggest difference, and the solution now runs well on all platforms.
Background jobs, though, were a different story. They needed threading frameworks only available to native code, and they were well suited to an experiment: a job manager written natively.
Writing the Job Manager in Kotlin Multiplatform
Shopify had two paths: write a new job manager in Kotlin for Android and port it to Swift for iOS, or adopt Kotlin Multiplatform Module (KMM) to share one codebase that runs on Android and iOS. KMM allows a single implementation but marks any platform-specific code—file I/O, network I/O, hardware access, OS features like Bluetooth or shared preferences—as expect and actual classes, or as injected dependencies conforming to a protocol.
The job system fit KMM well. Background jobs formed a defined graph—fetching, persisting, and removing stale data—that could be written purely in the Kotlin standard library. Platform-dependent pieces were handled by open-source libraries with KMM support, such as SQLDelight for the SQL database and Apollo for GraphQL requests; the remaining wrappers (file I/O, SharedPreferences/NSUserDefaults, background thread handling) were written in house on top of Kotlin’s coroutines.
Nearly all of the code lives in a shared, OS-agnostic common/ folder. At the time of writing, common/ holds about 3,447 lines of code, while the iOS and Android platform folders contain only 170 and 178 lines, respectively—roughly 95% code sharing. Performance improved dramatically: a medium-sized store’s initial sync that previously took at least 30 seconds now completes in 2-3 seconds. Existing JS code also served as a reference, improving the architecture and code organization of the native implementation.
Integrating KMM With an Existing React Native App
There are two ways to add KMM to your project:
- Add KMM build steps directly to the Android project that is part of your React Native app.
- Create a separate KMM project and import it as a library.
Shopify recommends the second option. Treating the KMM module as a separate library enforces clear boundaries between module and application, and it allows building small sandbox Android and iOS apps to test the library in isolation. The documented setup with IntelliJ is the general path, but Shopify’s more manual steps worked for them—assuming you start from an existing React Native application and want to generate an iOS framework.
Step 1: Organize Project Folders
Place the “native-sync” project as a subfolder named multiplatform/ at your project’s root.
Step 2: Register the Library With the Android Project
Add the library folder in android/settings.gradle and add it as a dependency in build.gradle.
Step 3: Create the KMM Library
In multiplatform/native-sync/, make the KMM project compile both a sandbox app and the exported library. Gradle nuances are too numerous to exhaustively cover here, but if manual configuration fails, the IntelliJ wizard is the recommended route. Shopify chose to create and edit gradle files manually for greater control and understanding.
Step 4: Build the Framework and Wire Up Native Modules
To build the iOS framework, run:
/gradlew :common:packForXcode -Pkn.iOS.target="arm"
The output is a framework in common/build/. Include it in your Xcode project, and run this command as part of the Xcode build by adding a New Run Script Phase in the Build Phases tab. From there, launch native modules in both the React Native Android and iOS apps—these act as intermediaries between React Native and the KMM library or framework.
Trade-offs and Learning Curves
For the most part, writing Kotlin Multiplatform Modules feels indistinguishable from writing Kotlin for Android, except that you are limited to the standard library. Developers coming from an iOS background face a steeper climb than their Android peers: they first have to learn React Native, TypeScript, and that entire ecosystem, and only then can they start on Kotlin—plus the particular subset supported by Kotlin Multiplatform. Android engineers already have the Kotlin foundation in place and can largely transfer their skills; those with a Java background have less of an advantage.
With the bulk of the background sync work behind them, Shopify’s mobile team considers Kotlin Multiplatform a viable option for most scenarios where native code is required. The team’s engineers come from either Swift or Kotlin backgrounds. Although Swift developers start at a disadvantage relative to Kotlin developers, most agree the two languages are similar enough in design and capability that the transition is manageable.
Open Source Considerations
This work depends heavily on open source projects, which always raises the question of long-term maintenance: will the project be sustained, and could we fork or maintain it ourselves if needed? React and React Native are backed by Meta (then Facebook), and both are enormously popular; JetBrains has had nothing but success with Kotlin. It seems unlikely that either organization will abandon these projects. Even Kotlin Multiplatform, a relatively new addition, has already been adopted by a wide range of companies and projects.
React Native is often associated with speed of development and cross-platform reach, but native code approaches still earn their place in production apps. The decision of when to drop down to native code—and how to manage that code once you do—deserves explicit planning rather than defaulting to whichever path seems fastest.



