Understanding the Migration: Flutter to React Native
Migrating a production mobile application from Flutter to React Native is a strategic decision that involves rebuilding the user interface, business logic, and native integrations in a JavaScript-driven ecosystem. This guide provides a complete, step-by-step walkthrough to help you plan and execute the transition with minimal friction.
Why Consider Migrating?
Flutter and React Native both enable cross-platform mobile development, but they differ fundamentally in language, rendering engine, and ecosystem. Common reasons teams move from Flutter to React Native include:
- JavaScript/TypeScript ecosystem – sharing code with web React projects, leveraging a massive npm package library, and using familiar tooling.
- Web compatibility – React Native for Web allows rendering components in browsers, unifying mobile and web codebases.
- Community and hiring – a larger pool of JavaScript developers compared to Dart specialists.
- Third‑party SDK availability – many services offer React Native SDKs as first-class citizens.
- CodePush and over‑the‑air updates – enabling instant hotfixes without app store review.
What to Expect During Migration
Flutter uses its own rendering engine (Skia/Impeller) and a widget tree architecture. React Native relies on native platform views bridged to JavaScript through a batched message queue, or more recently via Fabric and JSI for direct native access. Key differences include:
- UI composition: Flutter uses declarative widgets with a mutable state object; React Native uses React components with props and state.
- Styling: Flutter has a rich built-in theming system; React Native uses CSS-like style objects and libraries like StyleSheet.
- State management: Flutter commonly uses
setState, BLoC, Provider; React Native uses hooks (useState,useReducer) and libraries like Redux or Zustand. - Navigation: Flutter’s Navigator 2.0 vs React Navigation stack.
- Native modules: Flutter uses platform channels; React Native uses native modules and Turbo Modules.
Step-by-Step Migration Guide
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →1. Audit Your Flutter Project
Before writing any React Native code, perform a thorough inventory:
- List all screens and their widget trees.
- Document state management approach (BLoC, Provider, Riverpod, etc.).
- Identify third‑party Flutter packages and find equivalent React Native libraries.
- Map all platform‑specific features (camera, location, payments) and note whether they require native modules.
- Extract network layer, data models, and business logic that can be shared conceptually.
A detailed audit prevents surprises and helps you estimate the effort for each module.
2. Set Up the React Native Environment
Install Node.js, watchman, and the React Native CLI or Expo. For a bare workflow project:
# Initialize a new React Native project
npx react-native init MyApp --template react-native-template-typescript
# Or use Expo for managed workflow
npx create-expo-app MyApp --template expo-template-blank-typescript
Set up your preferred navigation library immediately:
npm install @react-navigation/native @react-navigation/stack
npm install react-native-screens react-native-safe-area-context
3. Recreate the UI Structure Screen by Screen
Begin with the most critical screen, usually the home or authentication screen. Map Flutter widgets to React Native components:
- Scaffold / AppBar →
SafeAreaView+ custom header component or React Navigation header. - Container →
Viewwith style props. - Text →
Textcomponent. - Column / Row →
ViewwithflexDirection: 'column'or'row'. - ListView →
FlatListorScrollView. - GestureDetector / InkWell →
TouchableOpacity,Pressable.
Example conversion from a simple Flutter widget to a React Native component:
// Flutter (Dart)
class ProfileCard extends StatelessWidget {
final String name;
final String imageUrl;
const ProfileCard({required this.name, required this.imageUrl});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(backgroundImage: NetworkImage(imageUrl)),
SizedBox(width: 12),
Text(name, style: TextStyle(fontSize: 18)),
],
),
);
}
}
// React Native (TypeScript)
import React from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
interface ProfileCardProps {
name: string;
imageUrl: string;
}
const ProfileCard: React.FC = ({ name, imageUrl }) => (
{name}
);
const styles = StyleSheet.create({
container: { padding: 16, flexDirection: 'row', alignItems: 'center' },
avatar: { width: 48, height: 48, borderRadius: 24 },
name: { fontSize: 18 },
});
export default ProfileCard;
4. Map Flutter State Management to React Hooks or Libraries
If your Flutter app uses setState and StatefulWidgets, the equivalent is the useState hook. For more complex state (BLoC, Provider), consider React counterparts:
- BLoC / Riverpod → Context +
useReduceror Redux Toolkit. - Provider → React Context or Zustand.
- ChangeNotifier → Observable patterns with MobX.
Example: Flutter BLoC vs React Context + useReducer
// Flutter BLoC (simplified)
class CounterCubit extends Cubit {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
// Usage: BlocBuilder(builder: (context, state) => Text('$state'))
// React Native with useReducer and Context
import React, { createContext, useReducer, useContext } from 'react';
type Action = { type: 'INCREMENT' };
const counterReducer = (state: number, action: Action): number => {
switch (action.type) {
case 'INCREMENT': return state + 1;
default: return state;
}
};
const CounterContext = createContext<{
count: number; dispatch: React.Dispatch;
}>({ count: 0, dispatch: () => {} });
export const CounterProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
{children}
);
};
// In a component:
const { count, dispatch } = useContext(CounterContext);
5. Handle Navigation and Deep Links
Replace Flutter’s Navigator (or go_router) with React Navigation. Configure a stack navigator:
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
function AppNavigator() {
return (
);
}
For deep links, use the linking configuration of React Navigation, which directly maps URL patterns to screens, analogous to Flutter’s onGenerateRoute.
6. Convert Data and Network Layers
Extract the API communication layer. Flutter often uses http or dio packages; React Native can use fetch, axios, or react-query.
// Flutter (dio)
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final response = await dio.get('/items');
final items = (response.data as List).map((e) => Item.fromJson(e)).toList();
// React Native (axios with TypeScript)
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';
const apiClient = axios.create({ baseURL: 'https://api.example.com' });
const fetchItems = async () => {
const { data } = await apiClient.get- ('/items');
return data;
};
const useItems = () => useQuery({ queryKey: ['items'], queryFn: fetchItems });
7. Integrate Native Modules and Permissions
For platform features (camera, geolocation, Bluetooth), replace Flutter plugins with equivalent React Native community packages. Example mapping:
- image_picker →
react-native-image-picker - location →
react-native-locationorexpo-location - shared_preferences →
@react-native-async-storage/async-storage - firebase_core / firebase_auth →
@react-native-firebase/appandauth
If a package doesn’t exist, write a native module using the React Native bridge or Turbo Modules. Keep the API contract similar to reduce refactoring.
8. Testing, Debugging, and Gradual Rollout
React Native offers Flipper, React DevTools, and react-native-debugger. Write unit tests with Jest and component tests with React Native Testing Library. Start with a phased migration strategy:
- Module‑by‑module replacement: Rewrite one feature at a time, releasing both apps in parallel if possible.
- Web‑first approach: Build shared components for web and mobile, then replace Flutter screens with React Native equivalents.
- Use CodePush to ship JavaScript updates instantly, reducing risk during rollout.
Best Practices for a Smooth Transition
- Keep business logic platform‑agnostic: Extract pure functions and data models into a shared JavaScript package that can be reused across React Native and any future web app.
- Adopt TypeScript from day one: It provides a type‑safety net similar to Dart’s strong typing, easing the mental shift.
- Use a design system: Map Flutter theme colors, typography, and spacing into a
StyleSheetor a library likestyled-componentsto maintain visual consistency. - Leverage React Native’s new architecture: Enable Fabric and Turbo Modules (on by default in recent versions) to improve UI performance and native module communication.
- Automate with CI/CD: Run linting, tests, and build checks on every commit. Use Fastlane for store deployments.
- Document the mapping: Maintain a living document that maps each Flutter widget, package, and navigation route to its React Native counterpart. This helps onboarding and code reviews.
Common Pitfalls and How to Avoid Them
- Over‑engineering the UI: Resist copying every single widget; instead, focus on component composition and reuse.
- Ignoring performance profiling: Use React Native’s performance monitor and
why-did-you-renderto catch unnecessary re‑renders. - Mixing navigation paradigms: Stick to a single navigation library (React Navigation) to avoid complexity.
- Neglecting native side updates: When replacing Flutter plugins, ensure native dependencies (Podfile, build.gradle) are correctly configured and updated.
Conclusion
Migrating from Flutter to React Native is a substantial undertaking that touches every layer of your application—UI, state, navigation, and native integrations. By systematically auditing your Flutter codebase, methodically rebuilding screens with React Native components, mapping state management to hooks or libraries, and leveraging the mature React Native ecosystem, you can achieve a successful transition. Follow the step‑by‑step process outlined here, embrace TypeScript, and adopt best practices like modular architecture and phased rollout. The result is a maintainable, JavaScript‑powered codebase that opens doors to web sharing, faster hiring, and a vast library of community‑driven modules.