This article compares a complete rewrite with a gradual migration when moving native iOS and Android apps to React Native. It explores the trade-offs in cost, time, and risk, along with the architectural dependencies that shape the migration path.

You have a product with an established user base and a proven market. But if your app is maintained in separate native iOS and Android codebases, you’re likely facing some familiar challenges: longer release cycles for new features and the ongoing overhead of supporting two codebases and keeping cross-platform development aligned .

At some point, you may consider migrating it to a cross-platform framework like React Native. But the main question is how to approach it: replace the existing implementation through a complete rewrite or transition to React Native incrementally? Let’s take a closer look.

The Dilemma: Rewrite or Migrate?

You already have a production mobile application for iOS and Android, implemented in Kotlin and Swift, respectively. The app is in demand and has an established user base that generates business value. Therefore, the decision to replace the existing implementation requires careful planning.

The objective of adopting React Native is to consolidate shared functionality into a common codebase, reduce development effort for new features, and implement them faster on both platforms.

And this is where the main dilemma arises: should you replace the existing implementation entirely or migrate gradually?

The answer depends  on several factors, including the budget you can allocate, the size of your team and their experience with React Native, the number of active users the app has, and the time required for implementation.

This isn’t just a technical decision – it’s a  trade-off between engineering efficiency, business continuity, cost, risk, and time to market.

What Does “Hybrid” React Native Actually Mean?

Both a complete rewrite and a gradual migration can involve a hybrid approach, where native and React Native functionality coexist during the transition.

This means you don’t have to take your existing application offline or replace its entire codebase at once. Instead, you can continue supporting the native app while introducing React Native features incrementally. The overall process looks like this:

In other words, you take a section of the code responsible for a specific feature and start converting it to React Native without immediately modifying or removing the original implementation. Once the React Native version is ready, you test it and implement it in the code. Next, monitor the feature in production for errors and bugs before removing the original native implementation; if everything is fine, then we can keep the React Native portion and remove the original native block.

At this point, you might ask: what’s the difference between a gradual rewrite and a complete rewrite if both involve a hybrid approach? The distinction largely comes down to the migration scope and the intended end state, but we’ll discuss the pros and cons of each approach in more detail later.

The key difference is the pace of migration and the target architecture: a rewrite aims for a fully migrated React Native application, while a gradual transition can stop at any stage.

Red Pill: Complete Rewrite

The complete rewrite approach is when the app is completely migrated from native code to React Native.

In practice, there are two main strategies for carrying out a complete rewrite.

Parallel Development Across Three Codebases

As we saw in the previous section, you don’t delete an existing code as soon as its React Native equivalent is ready, nor do you rewrite the native part all at once – you build the equivalent, test it, and only then integrate it. In practice, this means you now have three codebases: Swift, Kotlin, and React Native. You maintain and debug the existing app while gradually rewriting its functionality in React Native. When adding a new feature, you may need to implement it in all three codebases to keep all platforms up to date, but this can become costly and labor-intensive: a lot of time is spent maintaining the old code alongside the migration.

Incremental Migration

The second strategy is more gradual: continue supporting the native applications while implementing new features in React Native and migrating existing functionality incrementally. This approach provides more control and a smoother, more predictable transition.

Shopify is a good example of a strategy shift. Initially, they didn’t plan to use React Native, but later, as the framework matured, they decided to switch to it entirely. At first, the team chose the first approach, parallel development across three codebases, because they had no experience with React Native and were still adding new features to the native part of the app. Later, Shopify realized that this approach significantly slowed down development and complicated maintenance, so they switched to the second strategy.

Complete Rewrite Advantages and Risks

Why do companies choose a complete rewrite in the first place? It’s not solely because of performance. The real reasons are different: a cleaner architecture, a shared development stack, greater code reuse, and less need to maintain separate  UIs for different technologies in the future. This also includes faster development overall: the Android and iOS teams merge into a single React Native migration team, and a unified codebase helps prevent situations where the same feature is implemented with different business logic across platforms, making the code easier to understand and maintain.

When it makes sense to rewrite the code:

  • You have an old/legacy codebase
  • There are significant architectural issues
  • A major redesign is planned
  • The current native app is more expensive and more difficult to maintain than to rewrite
  • The company can afford a lengthy migration period
  • The product roadmap allows for a temporary slowdown

What risks might arise:

  • A long development cycle
  • Feature freezes or slower feature delivery
  • Regressions
  • Loss of some existing native functionality
  • The team needs to learn React Native from scratch
  • The new app risks reproducing old problems, but within a new tech stack

A rewrite may be the right solution, but the business must be prepared to bear the costs of the transition.

Blue Pill: Gradual Transition

There are two closely related strategies here: either you migrate existing functionality to React Native while continuing to develop new features in both the native and React Native versions, or you adopt a more aggressive approach – implementing all new features exclusively in React Native while gradually migrating existing functionality. The second option makes sense when you want React Native to become the primary technology for new feature development.

In practice, a gradual transition usually involves:

  1. Integrating React Native into the existing application
  2. Implementing a single, isolated feature in React Native
  3. Integrating with native navigation
  4. Migrating new screens and features
  5. Gradually reducing the proportion of native code
  6. Retaining native code only where it remains technically justified

A good example of this transition is Discord: the team gradually migrated the mobile app to React Native and, by virtualizing the list of channels and chats, improved scrolling performance. At the same time, they retained the emoji picker as a native component, because on lower-end devices, users could encounter blank frames while scrolling through emojis. Discord addressed this issue in the native Android implementation. This illustrates an important point: not every component  needs to be migrated.

How to Choose the Right Migration Strategy?

Both approaches can work, but the right choice depends on your budget, team experience, application complexity, and business priorities. A complete rewrite typically requires a larger upfront commitment, while gradual migration spreads the investment and risks over time.

Budget and Development Costs

A complete rewrite may require maintaining three codebases: Swift, Kotlin, and React Native, while the replacement is being developed. Beyond implementation costs, companies must account for ongoing native maintenance, team training, and potentially additional specialists.

Gradual migration allows companies to spread development costs across smaller iterations and see results earlier. However, it isn’t necessarily cheaper overall: maintaining both native and React Native functionality for an extended period can create comparable long-term expenses.

Team Size and Experience with React Native

A team already familiar with React Native may be better positioned to undertake a complete rewrite, especially if native developers can transition into a shared cross-platform team.

For teams without React Native experience, gradual migration provides an opportunity to build expertise through isolated features before committing to larger architectural changes. This reduces the risk of making costly decisions while the team is still learning the framework.

Application Criticality and Risk

The larger and more business-critical the application, the greater the potential impact of migration-related regressions. Replacing substantial functionality at once may introduce issues affecting a significant portion of the user base.

Gradual migration allows teams to introduce changes incrementally, monitor production metrics, and roll back problematic features. However, it also creates integration risks, since native and React Native functionality must coexist throughout the transition.

Neither approach eliminates risk. A complete rewrite concentrates more risk around the replacement and rollout, while gradual migration distributes it across multiple integration and release cycles.

Timeline and Product Roadmap

A complete rewrite can demand a significant engineering commitment over months or even years, depending on the application’s complexity. During this period, the team must balance maintaining the existing product with developing its replacement, potentially slowing down feature delivery.

Gradual migration is often easier to accommodate alongside an active product roadmap. Teams can continue shipping new features while migrating existing functionality in stages, although this flexibility may extend the overall transition period.

Existing Codebase and Architecture

The condition of the current native application also matters. If the codebase has accumulated substantial technical debt, suffers from architectural limitations, or requires a major redesign, a complete rewrite can provide an opportunity to establish a cleaner foundation.

However, adopting React Native does not automatically resolve existing architectural problems. Those issues must be addressed deliberately during the redesign.

If the native application is stable and well-structured, gradual migration may be more practical, allowing the team to reduce duplicated development effort without replacing functionality that already works well.

Ultimately, the decision comes down to balancing cost, time, and risk. A complete rewrite may be appropriate when the business can support a larger initial investment and temporary slowdown. Gradual migration is often better suited to products that must continue evolving while minimizing the impact of architectural changes on existing users.

Hidden Costs and Challenges of Gradual Migration

Gradual migration reduces the risk of replacing an entire application at once, but it introduces a different kind of complexity. For some time, the product effectively runs across two technology stacks: part of the application remains native, while another part runs on React Native.

One way to prioritize features is the RICE framework, which evaluates Reach (how many users a feature affects), Impact (its expected value), Confidence (how reliable the estimates are), and Effort (the resources required). This helps teams identify features that offer the greatest potential value relative to their implementation costs.

However, business value alone should not determine migration order.

The main challenge is therefore not simply integrating React Native into an existing application but determining what to migrate first and in what order.

At first, starting with a few small and low-risk screens may seem like the safest approach. In practice, however, the smallest screen is not always the best migration candidate. A screen may look isolated from the user’s perspective while still depending heavily on native navigation, shared services, authentication state, analytics, deep links, or other infrastructure that remains implemented in the existing architecture. Migrating such a screen too early can result in temporary integration code that later has to be rewritten once the underlying infrastructure is migrated.

Instead of looking only at individual screens, teams should first identify the architectural dependencies that connect them.

Navigation is a good example of why migration order matters.

If most of the application depends on a native navigation layer, migrating screens one by one without a defined integration strategy may require additional adapters between React Native and native routes every time a new feature is moved. Therefore, in some applications, it may make more sense to establish the navigation boundary first and define how React Native screens participate in the application’s routing model.

Once that foundation is stable, migration can proceed more predictably, for example:

Native application → shared navigation boundary → React Native feature → React Native screen → another React Native screen → progressively smaller native surface

This does not necessarily mean that the entire navigation system must be rewritten before the first React Native screen is introduced. The important part is to define the target architecture and integration boundaries early enough to avoid rebuilding the same bridge repeatedly.

Navigation is usually one of the first areas where a brownfield application becomes more complicated.

During migration, the application may temporarily have two navigation systems:

  • Native routes controlling existing screens
  • React Native navigation controlling migrated screens

The difficult part is not opening a React Native screen from native code. The real complexity appears when users need to move freely across the boundary.

For example: Native Screen -> React Native Screen -> Native Screen -> React Native Modal

Back navigation, route parameters, authentication redirects, modal presentation, tab state, and navigation history must remain predictable regardless of where the screen is implemented.

Without a clearly defined navigation contract, teams can quickly accumulate route adapters and platform-specific exceptions that become increasingly difficult to maintain.

Deeplinking

Deep linking exposes the same problem from another direction.

A URL such as myapp://orders/123 should open the correct destination regardless of whether the Orders screen is still native or has already been migrated to React Native.

During a gradual migration, this means the application needs a consistent mechanism for resolving incoming links and determining which implementation should handle each destination.

Otherwise, teams can end up maintaining duplicated routing rules in native code and React Native, increasing the risk of inconsistent behavior across platforms or between different entry points into the application.

This becomes especially important when deep links are used by push notifications, marketing campaigns, authentication flows, universal links, or app links.

Safe Area

Safe Area handling is another issue that appears simple until native and React Native screens start living next to each other.

Native containers may already apply system insets for status bars, navigation bars, notches, or home indicators. A React Native screen embedded inside that container may apply its own Safe Area handling again.

The result can be duplicated padding, inconsistent headers, content appearing underneath system UI, or slightly different layouts between migrated and native screens.

These issues are rarely architecturally critical, but they have a direct impact on perceived product quality. During a long migration, inconsistencies between native and React Native screens can make the application feel fragmented even when both implementations work correctly.

Teams should therefore decide early which layer owns system insets, headers, status-bar behavior, and other shared layout responsibilities.

Two Stacks Temporarily Mean Two Sources of Complexity

The transition period also means that the team has to support both ecosystems. Depending on the architecture, this can include:

  • Native and React Native navigation
  • Communication across the native – React Native boundary
  • Duplicated or adapted state management
  • Shared authentication and session state
  • Analytics and logging across both environments
  • Native modules and React Native dependencies
  • Build configuration for both platforms
  • CI/CD changes
  • Debugging issues that cross the JavaScript/native boundary
  • Maintaining consistent UI components and design tokens

Some duplication during migration is unavoidable. The goal is not to eliminate it completely, but to make sure that temporary migration infrastructure does not accidentally become permanent architecture.

Conclusion

Both complete rewrites and gradual migrations offer viable paths to React Native, but each comes with different trade-offs. A complete rewrite provides an opportunity to establish a cleaner architecture and unified technology stack, while gradual migration spreads costs and risks over time, allowing existing functionality to remain in production.

However, one of the less obvious risks of gradual migration is that it can become permanent. Without a clearly defined target architecture, teams may continue introducing React Native features while leaving outdated native infrastructure untouched, resulting in a mixed codebase that requires long-term maintenance of both stacks.

Retaining selected native components is not necessarily a problem. What matters is whether they remain because they provide technical value or simply because the migration was never completed.

That’s why a successful migration starts not with choosing which screen to rewrite first, but with defining the target architecture. Understanding architectural dependencies, integration boundaries, and which components should remain native makes the migration easier to prioritize and manage.

Ultimately, the goal isn’t to migrate as much code as possible. It’s to build a maintainable architecture that reduces unnecessary duplication, supports faster feature delivery, and allows the product to evolve without disrupting ongoing development.

Check our blog to read more insights from our experts.

If you want to delegate React Native development or need tech advice, our senior React Native engineers can help.