Migrating our Largest Mobile App to React Native

Migrating our Largest Mobile App to React Native

In 2020, we announced that React Native is the future of mobile at Shopify and since then we’ve been migrating all our native mobile apps to React Native. Since each app is different, there is no single approach that works for all of them. So, we evaluated all the possible options for each app and chose the ones that best suit their needs.

Shopify Point of Sale, for instance, had come a long way since it was first built during an internal hackathon. It was originally designed and built to support small mom-and-pop stores or weekend warriors. However, it has surged in popularity and is being used by some of our biggest merchants and is processing transactions worth billions of dollars each year. The codebase had accumulated a lot of tech debt and the app’s UX was also not serving the needs of large merchants who have hundreds of locations and tens of thousands of products. After a thorough evaluation, it became clear that we couldn’t fix these issues with incremental changes. Hence, we decided to do a full rewrite, which has been a big hit with our merchants.

Shopify Mobile, our flagship mobile app, on the other hand is quite stable and meets our merchants’ needs. It is also our largest app at 300 screens per platform and took over six years to build. Rebuilding it from scratch would be a massive undertaking. Even if we assume that we’d be twice as productive with RN (which is not necessarily the case always), it would take us at least three years to rebuild. That’s a very long time. We would have to halt all new feature development during this time and in the end have the exact same app as we started with. A rewrite then, was clearly out of question.

Build and Migrate, All at Once

Having all mobile teams use a single tech stack and tooling across the company gives you an incredible amount of leverage. We didn’t want Shopify Mobile to miss out on all the shared libraries, components, and tooling that other apps were benefiting from. So, we decided to gradually start adopting React Native in the app instead of doing a full rewrite.

Our Mobile Enablement team started laying the foundation to introduce React Native into the codebase. Feature teams then started building new features in React Native whenever possible. While doing so, they also migrated existing parts of the codebase to React Native as needed.

This approach worked quite well for a while. Our team was very new to React Native and it allowed us to learn and experiment with different approaches to app architecture. It also allowed us to avoid creating technical debt while building a lot of features without having the same level of expertise in React Native as we do with native technologies.

However, this approach had several drawbacks:

  1. Migration was extremely slow and would have taken us four to five years to complete.
  2. Any change to the design system had to be done three times—once in native Android, once in native iOS, and once in React Native.
  3. We’d have to maintain three different architectures (Android, iOS, RN) until the migration was complete.
  4. Maintaining interoperability with existing native code became very time-consuming.

A few months into the migration, it became clear that this strategy was no longer serving us well. It was optimized for delivering merchant value but severely limited migration efforts, making it slow and non-deterministic. We needed to rethink how to move forward.

A lot had changed since we first chose that approach. We had a solid foundation to build complex features using React Native, we had full support from Engineering, Product, and UX leadership to migrate the app, and most developers on the team were on board with switching to React Native.

After evaluating several options, we decided to go with an approach we like to call “Iterative Porting”. In this approach we started building all new features in React Native and migrating existing features in parallel.

Teaching Everyone to Ship in React Native

To implement this plan, we first needed to make sure that all the engineers on our team knew how to write React Native code. They all had extensive experience building mobile apps using Kotlin and Swift, but React Native was a completely new tech stack for many of them.

To make this transition easy for everyone, we worked with our Learning & Development team to create an internal training program called RN Accelerator.

This program was designed to cover everything that an engineer needs to know to be able to ship React Native code in Shopify Mobile. It consists of five milestones:

  1. Javascript and Typescript
  2. React
  3. React Native
  4. React Native for Shopify Mobile
  5. Advanced tools and techniques

Dividing the program this way allowed the team to skip the topics they already knew and spend more time learning the rest. We started tracking how people were progressing through this program and awarded swag on completion. Soon, all the engineers were fully trained and started shipping React Native code to production.

Who, What, When? Identifying and Prioritizing What to Migrate 

We wanted to identify all the surfaces that need to be migrated to React Native so that we could use them to plan our work and measure our progress. To do this, we wrote a custom script that looked at our iOS and Android code and identified all the source files that would have to be migrated.

We then populated these files into a spreadsheet and categorized them by feature and sub-teams. We also added columns to indicate complexity and status. Each sub-team had a clear, well-defined scope for the migration. There were a few surfaces that were no longer owned by any teams, so we identified new owners for them.

Migration Criteria

We had a giant list of surfaces to migrate at this point and needed a way to prioritize it. We used a rough prioritization framework (based on the RICE scoring model) to ensure that we were prioritizing the right things doing so consistently across all sub-teams. For each item on the list, we determined its:

  1. Reach: The number of monthly active users using this feature
  2. Impact: The qualitative or quantitative impact of this feature (1-5, 1 being lowest impact)
  3. Confidence: How confident you are about the reach and impact (percentage)
  4. Effort: The amount of effort it’ll take to port this feature (how many people over how many weeks)

Then, the RICE Score was calculated as:

(Reach x Impact x Confidence) / Effort = RICE Score

We use this score to stack-rank features and components and schedule time to port them in each development cycle and update their status in the spreadsheet.

As we migrate surfaces that have been deemed eligible, we ensure there aren’t missing dependencies and move through the process of migrating (with a feature flag), testing, and shipping to production. We monitor each migrated feature for two weeks to ensure stability before deleting the native version and removing the feature flag, then marking the migration as complete.

Making Improvements As We Go

As we port screens to RN, we also look out for opportunities to improve the UX of the app. This way, we can port the app to React Native without reducing the speed of innovation for our merchants. The Discounts List is a good example. We wanted to modernize the search experience and make it similar to other screens in the app. Since a new component was created to render lists and filters, we used the React Native port as a reason to implement the new design from the start.

Screenshot comparing the previous discount index to the new version.
Screenshots comparing the previous native Discount List to the new RN version.

Not every screen will be eligible for improvement though, and we’ve set up a decision flow that helps determine which improvements will be prioritized and which teams will own them. These decisions are made based on the type of improvement (for example, bug fixes, broken windows, or revising native components for RN) and which team has the most context and bandwidth to own the issue. In cases where there isn’t clear alignment on the solution or appropriate team to own the issue, a project proposal is required to further explore the potential improvement before moving forward. 

Where to Start? At the Beginning

You can’t improve what you can’t measure, this is why the first step was to map all the screens and to create a dashboard, where we can follow the migration in an easy way.

Screenshot of our migration tracking dashboard
Our dashboard for tracking the migration progress.

Now we only needed to choose where to start the migration from. We thought about starting with smaller screens, with less code and less impact. But after some discussion and prototyping we decided that we should go all-in and prioritize high-impact screens first, like the root screens of the app.

Today, the main tab-based navigation screen in Shopify Mobile is written in Kotlin and Swift. To increase React Native development within Shopify Mobile, more surfaces must be ported. To unblock developers from being able to extend and modify Shopify Mobile, the app needs to support React Native earlier—at the root screen after login.

Porting root overview screens will further increase the amount of React Native screens in the app. This will set up Shopify Mobile to be fully React Native, from app start to feature screen. It also enables us to do global configuration on the React Native side when the app starts, versus having to wait for the first React feature to be launched.

Screenshot of Root screen of the Shopify Mobile app.
Root screen of the Shopify Mobile app.

Benefits of the Move

We had clear goals going into the project, but beyond those, we experienced some additional benefits worth mentioning here.

Reducing Implementation Differences Between iOS and Android

We always knew some business logic was different on iOS and Android, but while porting the screens we realized that it was more than expected. The way we rendered product subtitles, the permissions that were being checked to display certain components, the fields used to query data in GraphQL, etc. Aligning all of these will make it way easier for us to implement new features on top of them later.

More Hands Make Lighter Work

iOS and Android developers came together and became mobile developers, doubling the total number of developers we have to work on features, approve PRs, etc. This increased the speed of new feature development. Another goal is to make it easier for web developers to contribute to Shopify Mobile, which will increase even more the number of developers contributing to the app.

Simpler Code

Moving from an Imperative UI implementation, in both iOS and Android, to a Declarative UI made our code way simpler. Also, starting from scratch allowed us to be more intentional about the logic and remove unnecessary variables that were being queried from GraphQL, clean up old experiments, and fix some tech debt.

Where We Are Now

Here’s a spoiler: Today, if you open the Shopify Mobile app, all four root screens are in React Native already! So, it’s time for a challenge. Which one of the screens in the video below is native, and which one is React Native?

Video demo of the Shopify Mobile app (React Native is on the left and native on the right. Did you guess correctly?)

It is hard to tell right? To be honest, I didn’t even remember which one was which! This was one of the main goals of the project. Some users might notice a change, usually for the better because we’ll also introduce improvements in some screens, but most screens will look and feel the same.

A Few Bumps in the Road

The project to port the root screens took around four months. Most of it was preparing the necessary infrastructure, which is a worthwhile investment because it will end up being used by all the other screen ports going forward. Shopify is also investing heavily in open-source frameworks for React Native that we also use internally, for example:

There were certainly some challenges we were already expecting and preparing for, but some came as surprise during the project. Here are some of the challenges we solved and lessons we learned.

Native Routes

While porting the root screens, we needed a way to open the inner screens from React Native. Usually these screens would open straight from the View Controller and Fragment in native. We had to build a way to open them from RN, with a custom name and implement them in both Android/iOS. We called these Native Routes. With a custom implementation, we now have a navigator in React Native that allows us to navigate to native screens. This allowed us to completely remove the backing View Controller and fragments, removing as much specific native code as possible per screen.

Deeplink Support

Deeplink was not a problem in Android, where we were opening the desired screen directly as a modal. But on iOS it was a different story—we were building the entire navigation stack to get to that inner screen. This meant that all the root screens we were trying to port were also the entry point to all deeplinks in the app. At first we tried to follow the old behavior by recreating the entire navigation stack. The thing is, since other screens were being ported at the same time, there were some really complex cases where we would have RN->Native->Native->RN for example. So, we decided against this solution and settled on iOS following what Android was already doing, which is to open the deeplink as a modal. Since we were already implementing the Native Routes for each one of the inner screens, this was fairly simple and we were able to implement it pretty fast.

Safe Area

Another thing we found difficult in the beginning was determining how to properly support Safe Area. Our first instinct was to just wrap the outer components in a SafeAreaView, but then we realized that for some of them, especially for the horizontal safe area support, we would need to wrap the content of each cell, not the cell itself. This is because the background of the cell was supposed to not respect the Safe Area, just the content inside. Once we realized most of the Safe Area fixes should be done inside our UI components, things were easier.

Screenshot demonstrating incorrect vs correct SafeArea configuration,
Incorrect vs correct SafeArea configuration (note the grey padding in the incorrect version).

Looking Ahead

Now that our root screens are ported and most of the necessary infrastructure is in place, I’m noticing that the ports are picking up speed! Most of our developers didn’t know React Native before this project, so each day they learn more, which further contributes to the fast pace. Working on a project like this is challenging, because you’ll end up working with both native implementations plus React Native, but for those who love to code and are open to new challenges, it’s really rewarding. There’s still a long way to go, but based on what I’m seeing, the React Native Shopify of the future is much closer than we think.

Mauricio de Meirelles is a Staff Developer on Shopify’s Core Mobile team.

We all get shit done, ship fast, and learn. We operate on low process and high trust, and trade on impact. You have to care deeply about what you’re doing, and commit to continuously developing your craft, to keep pace here. If you’re seeking hypergrowth, can solve complex problems, and can thrive on change (and a bit of chaos), you’ve found the right place. Visit our Engineering career page to find your role.

Back to blog