HomeDocumentationc_004_flutter_enterprise
c_004_flutter_enterprise
13

Migrating from React Native to Flutter: A Practical Guide to Getting It Right

React Native to Flutter Migration: When and How to Do It

March 17, 2026

What React Native Actually Is

If you've used React for web, React Native is familiar and slightly disorienting at the same time. The component model is the same. The hooks are the same. useState, useEffect, useCallback — all exactly as expected.

What's different is what's underneath.

In React for web, your components render to HTML elements. In React Native, they render to native platform components — UIView on iOS, android.view.View on Android. A <View> in React Native becomes an actual native view managed by the operating system. A <Text> becomes a UILabel or TextView.

To make this work, React Native runs JavaScript in a JS engine (JavaScriptCore historically, Hermes in newer versions) in its own thread. When your JavaScript code changes state and triggers a render, it calculates the changes and sends instructions across a bridge to the native thread, which applies them to the native components.

That bridge is the source of most React Native performance problems.

Every interaction — a scroll, a gesture, an animation — crosses the bridge. At low frequency, the overhead is imperceptible. Under load, in complex animations, in gesture-driven interactions where frames need to be calculated at 60fps, the bridge latency accumulates. The JS thread and the native thread fall out of sync. The user feels it.

The React Native team addressed this with a new architecture — JSI, Fabric, TurboModules — which removes the serialisation overhead from the bridge and allows synchronous calls between JS and native. Many production apps are still on the old architecture. The new one helps significantly but doesn't eliminate the fundamental constraint: there is still a JavaScript runtime running your app logic on the device.

What Flutter Does Differently

Flutter doesn't use native platform components. This sounds like a disadvantage. It isn't.

Instead of asking iOS to render a UILabel and Android to render a TextView, Flutter renders everything itself — directly onto a GPU canvas using its own rendering engine, Impeller). Every button, every text field, every animation is drawn by Flutter. The platform provides a blank surface. Flutter paints on it.

The implication: there is no bridge. Your Dart code compiles to native ARM code — not bytecode, not a virtual machine, not a JavaScript runtime. It runs directly on the CPU, the same as native Swift or Kotlin code. The rendering instructions go directly to the GPU.

This is why Flutter animations are smooth on devices where React Native animations aren't. There is no layer of translation between the logic and the pixels. And because Flutter draws its own widgets, the UI looks identical on iOS and Android. Not "close enough" — identical. The platform doesn't influence the rendering.

The tradeoff: Flutter widgets don't automatically look like the platform's native components, because they aren't the platform's native components. If you need your app to feel genuinely native on each platform — following iOS Human Interface Guidelines in ways a user instinctively recognises — Flutter requires deliberate work to achieve that. Most apps don't actually need this, which is why most migrations don't regret it.

The Code, Side by Side

If you know React (even just the web version), Flutter's widget model is more familiar than it first appears. The concepts map closely. The syntax is different.

Here's a data-fetching screen in React Native:

javascript
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';

const OrderListScreen = () => {
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/orders')
      .then(res => res.json())
      .then(data => {
        setOrders(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <ActivityIndicator />;
  if (error) return <Text>{error}</Text>;

  return (
    <View style={styles.container}>
      <FlatList
        data={orders}
        keyExtractor={item => item.id}
        renderItem={({ item }) => (
          <View style={styles.card}>
            <Text style={styles.title}>{item.title}</Text>
            <Text style={styles.status}>{item.status}</Text>
          </View>
        )}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  card: { padding: 16, borderBottomWidth: 1, borderColor: '#eee' },
  title: { fontSize: 16, fontWeight: 'bold' },
  status: { fontSize: 14, color: '#666' },
});

And the Flutter equivalent, following the architecture from the Clean Architecture article:

dart
// The screen — thin, delegates to BLoC
class OrderListScreen extends StatelessWidget {
  const OrderListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: BlocBuilder<OrderBloc, OrderState>(
        builder: (context, state) => switch (state) {
          OrderLoading() => const Center(child: CircularProgressIndicator()),
          OrderError(:final message) => Center(child: Text(message)),
          OrderLoaded(:final orders) => ListView.builder(
              itemCount: orders.length,
              itemBuilder: (context, index) => OrderCard(
                order: orders[index],
              ),
            ),
          _ => const SizedBox.shrink(),
        },
      ),
    );
  }
}

// The card — its own widget, const-constructible
class OrderCard extends StatelessWidget {
  final Order order;
  const OrderCard({super.key, required this.order});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: const BoxDecoration(
        border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE))),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(order.title,
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          Text(order.status.name,
              style: const TextStyle(fontSize: 14, color: Color(0xFF666666))),
        ],
      ),
    );
  }
}

What transferred directly: the component-as-function mental model, the separation of concerns, the conditional rendering pattern. What changed: styling is widget composition instead of a StyleSheet, state management is BLoC instead of hooks, the API call moved to a use case and repository rather than living in the component.

A React Native developer can read Flutter code productively within a day or two. The concepts are close enough. The syntax is the friction, and friction dissolves with practice.

When You Should and Shouldn't Migrate

This needs to be said clearly: migration is not always the right answer.

Good reasons to migrate:

  • Persistent animation performance problems that profiling confirms are bridge-related
  • UI inconsistency between iOS and Android that the product team won't accept
  • The team is growing and TypeScript's type system is proving insufficient for the domain complexity
  • You're targeting platforms beyond iOS and Android (web, desktop) and want one codebase
  • The React Native version is old enough that upgrading it would be nearly as disruptive as rewriting

Bad reasons to migrate:

  • The app works fine and the team is comfortable with React Native
  • The rewrite is motivated by developer preference rather than user-facing problems
  • The business case is "Flutter is better" without specific problems to solve
  • The existing codebase has good test coverage and clean architecture — the investment is already there
Pro Tip
A Flutter rewrite of a working React Native app is months of work with no new features for users. That cost needs to be justified by real, measurable problems that the migration will actually fix.

Migration Strategies

The Full Rewrite

The clean approach. One codebase replaced by another over a defined timeline. The team pauses feature development, completes the rewrite, and ships the Flutter version.

Works best for: smaller apps (under ~30 screens), apps with poor existing architecture (a rewrite is needed regardless), companies with the runway to pause feature development.

The risk is the "feature parity trap" — the rewrite takes longer than expected, the old app keeps getting bug fixes, and eventually you're maintaining two apps instead of one. Set a deadline and stick to it. Imperfect Flutter beats an indefinitely delayed perfect Flutter.

Gradual Migration with Flutter Add-to-App

Flutter has a lesser-known capability: it can run inside an existing native or React Native app. Individual screens or flows are built in Flutter while the rest of the app remains in React Native. Users don't know the difference.

This is Flutter's Add-to-App feature. Flutter is embedded as a module, and the host app launches Flutter views as needed. For more detailed insights, refer to our overview of Flutter compilation

On Android:

kotlin
// In your Android Activity (React Native host)
class MainActivity : AppCompatActivity() {
    private lateinit var flutterEngine: FlutterEngine

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Pre-warm the Flutter engine for faster initial load
        flutterEngine = FlutterEngine(this).also {
            it.dartExecutor.executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
            )
            FlutterEngineCache.getInstance().put("order_engine", it)
        }
    }

    fun openFlutterOrderScreen() {
        startActivity(
            FlutterActivity.withCachedEngine("order_engine").build(this)
        )
    }
}

On iOS:

swift
// In your React Native AppDelegate or a coordinator
func openFlutterOrderScreen() {
    let flutterEngine = FlutterEngine(name: "order_engine")
    flutterEngine.run()

    let flutterViewController = FlutterViewController(
        engine: flutterEngine,
        nibName: nil,
        bundle: nil
    )
    present(flutterViewController, animated: true)
}

The Flutter screen launches, runs independently, and returns control to the React Native app when dismissed. You can pass data between them using MethodChannel — a thin message-passing API:

dart
// In Flutter — receive the order ID from React Native
const platform = MethodChannel('com.yourapp/orders');

Future<void> loadOrder() async {
  final orderId = await platform.invokeMethod<String>('getOrderId');
  context.read<OrderBloc>().add(OrderLoadRequested(orderId!));
}
javascript
// In React Native — launch the Flutter screen with context
import { NativeModules } from 'react-native';

const openFlutterOrder = async (orderId) => {
  await NativeModules.FlutterBridge.openOrderScreen({ orderId });
};

Works best for: large apps with clear feature boundaries, teams that need to keep shipping features while migrating, companies that can't justify a full pause.

The cost: two build systems, two codebases in parallel, added complexity at the boundary. This is manageable for a time-limited migration and expensive as a permanent state. The goal is to eat the app screen by screen until nothing remains in React Native.

What Transfers and What Doesn't

Transfers well:

  • The component/widget mental model — close enough to learn quickly
  • Business logic — if it was cleanly separated in React Native (services, hooks), it maps to use cases and domain classes
  • API integration patterns — REST calls, JSON parsing, authentication flows
  • Navigation concepts — stack navigation maps to Navigator, tab navigation maps to TabBar
  • TypeScript skills — Dart's type system is strict in similar ways, the concepts transfer even if the syntax differs

Doesn't transfer:

  • The styling model — no CSS, no StyleSheet. Everything is widget properties and layout widgets. This takes real adjustment.
  • The package ecosystem — npm has more packages. Most things you need exist in pub.dev, but you'll occasionally find something that doesn't yet.
  • Over-the-air updates — React Native apps can push JS bundle updates without app store review (via CodePush). Flutter apps compile to native code. Updates go through the store. If OTA updates are load-bearing for your release process, this matters.
  • Any native module you wrote in JavaScript — custom React Native bridges need to be rewritten as Flutter plugins or replaced with pub.dev equivalents.

The Business Case

The migration argument to a technical stakeholder is performance, consistency, and long-term maintainability.

The migration argument to a non-technical stakeholder is simpler: the current app has problems that fixing incrementally costs more than rebuilding correctly. Frame it in terms of user-visible outcomes — smoother animations, consistent design, fewer bugs — and timeline.

A realistic Flutter migration for a medium-sized React Native app (20–40 screens, moderate complexity) takes three to five months for a small team. The full rewrite approach front-loads the cost. The Add-to-App approach spreads it. Both require honest communication with stakeholders about what "done" means and what users will experience during the transition.

What they get on the other side: an app that performs consistently across the Android fragmentation landscape, a codebase where business logic and UI are cleanly separated, and a rendering engine that doesn't rely on a JavaScript runtime to produce pixels.

It's not always worth it. When it is, the difference is measurable — in frame times, in crash rates, in the time it takes to add the next feature without breaking the last one.

Related Topics

react native to flutter migrationmigrate react native to flutterflutter vs react native performancereact native bridge performance problemsshould i migrate from react native to flutterflutter vs react native 2026flutter add-to-app embeddingreact native to flutter rewriteflutter native arm vs javascript runtimeflutter migration strategyreact native flutter side by side

Ready to build your app?

Turn your ideas into reality with our expert mobile app development services.