Skip to content
Mobile

React Native - Building Mobile Applications in 2025

Published on:
·8 min read·Author: MDS Software Solutions Group

React Native Building

mobile

React Native - Building Mobile Applications

React Native is one of the most popular frameworks for building mobile applications for iOS and Android platforms using a single codebase written in JavaScript and TypeScript. Created by Meta (formerly Facebook) in 2015, this framework revolutionized mobile development by enabling web developers to build native applications without having to learn Swift or Kotlin from scratch. In this article, we will explore React Native's architecture, tooling, design patterns, and production-ready best practices in detail.

What is React Native?#

React Native is an open-source framework that allows you to build truly native mobile applications using React. Unlike hybrid solutions based on WebView (such as Cordova/PhoneGap), React Native renders native UI components specific to each platform. This means a button in React Native is a real UIButton on iOS and an android.widget.Button on Android.

import React from 'react';
import { View, Text, StyleSheet, Platform } from 'react-native';

const WelcomeScreen: React.FC = () => {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>
        Welcome to React Native!
      </Text>
      <Text style={styles.subtitle}>
        Platform: {Platform.OS} (v{Platform.Version})
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#333',
  },
  subtitle: {
    fontSize: 16,
    color: '#666',
    marginTop: 8,
  },
});

export default WelcomeScreen;

Key features of React Native include code sharing between platforms (typically 80-95% of code is shared), hot reloading for instant change preview, a rich library ecosystem, and a large and active developer community.

Expo vs Bare Workflow#

When starting with React Native, the first decision involves choosing between Expo and the bare workflow. Each approach has its advantages and limitations.

Expo - Quick Start#

Expo is a set of tools and services built around React Native that significantly simplifies the development process. The Expo SDK provides ready-to-use APIs for camera, location, push notifications, and many other features without requiring native code configuration.

// Initialize an Expo project
// npx create-expo-app@latest MyApp --template blank-typescript

import { StatusBar } from 'expo-status-bar';
import * as Location from 'expo-location';
import React, { useEffect, useState } from 'react';
import { View, Text, Alert } from 'react-native';

interface LocationCoords {
  latitude: number;
  longitude: number;
}

const LocationScreen: React.FC = () => {
  const [location, setLocation] = useState<LocationCoords | null>(null);

  useEffect(() => {
    const getLocation = async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();

      if (status !== 'granted') {
        Alert.alert('Permission denied', 'The app requires access to location');
        return;
      }

      const currentLocation = await Location.getCurrentPositionAsync({});
      setLocation({
        latitude: currentLocation.coords.latitude,
        longitude: currentLocation.coords.longitude,
      });
    };

    getLocation();
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      {location ? (
        <Text>
          Location: {location.latitude.toFixed(4)}, {location.longitude.toFixed(4)}
        </Text>
      ) : (
        <Text>Fetching location...</Text>
      )}
      <StatusBar style="auto" />
    </View>
  );
};

Since Expo SDK 49+, Expo Dev Client is also available, which allows you to use custom native modules while maintaining the convenience of Expo. Thanks to Continuous Native Generation (CNG), Expo automatically generates native iOS and Android projects based on the configuration in app.json.

Bare Workflow#

The bare workflow gives you full control over native iOS (Xcode) and Android (Android Studio) code. This is the preferred approach when your app requires advanced integration with native SDKs, custom native modules, or specific build configurations.

# Initialize bare workflow
npx react-native@latest init MyApp --template react-native-template-typescript

# Project structure
# ├── android/          <- native Android code
# ├── ios/              <- native iOS code
# ├── src/
# │   ├── components/
# │   ├── screens/
# │   ├── navigation/
# │   ├── store/
# │   ├── hooks/
# │   ├── services/
# │   └── types/
# ├── App.tsx
# ├── tsconfig.json
# └── package.json

In practice, Expo with Dev Client covers the majority of use cases and is the recommended approach for new projects. The bare workflow should only be considered for very specific native requirements.

React Native Components#

React Native provides a set of core components that map to native views. The most important ones are View, Text, Image, ScrollView, FlatList, TextInput, TouchableOpacity, and Pressable.

import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  FlatList,
  Pressable,
  Image,
  StyleSheet,
  ActivityIndicator,
} from 'react-native';

interface Product {
  id: string;
  name: string;
  price: number;
  imageUrl: string;
  description: string;
}

const PRODUCTS: Product[] = [
  {
    id: '1',
    name: 'ProBook Laptop',
    price: 1299,
    imageUrl: 'https://example.com/laptop.jpg',
    description: 'High-performance laptop for work',
  },
  {
    id: '2',
    name: 'Ultra Smartphone',
    price: 899,
    imageUrl: 'https://example.com/phone.jpg',
    description: 'Flagship smartphone with an amazing camera',
  },
];

interface ProductCardProps {
  product: Product;
  onPress: (product: Product) => void;
}

const ProductCard: React.FC<ProductCardProps> = ({ product, onPress }) => (
  <Pressable
    style={({ pressed }) => [
      styles.card,
      pressed && styles.cardPressed,
    ]}
    onPress={() => onPress(product)}
  >
    <Image
      source={{ uri: product.imageUrl }}
      style={styles.productImage}
      resizeMode="cover"
    />
    <View style={styles.cardContent}>
      <Text style={styles.productName}>{product.name}</Text>
      <Text style={styles.productPrice}>${product.price}</Text>
      <Text style={styles.productDescription} numberOfLines={2}>
        {product.description}
      </Text>
    </View>
  </Pressable>
);

const ProductListScreen: React.FC = () => {
  const [searchQuery, setSearchQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const filteredProducts = PRODUCTS.filter((product) =>
    product.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  const handleProductPress = (product: Product) => {
    console.log('Selected product:', product.name);
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.searchInput}
        placeholder="Search products..."
        value={searchQuery}
        onChangeText={setSearchQuery}
        autoCapitalize="none"
      />
      {isLoading ? (
        <ActivityIndicator size="large" color="#007AFF" />
      ) : (
        <FlatList
          data={filteredProducts}
          keyExtractor={(item) => item.id}
          renderItem={({ item }) => (
            <ProductCard product={item} onPress={handleProductPress} />
          )}
          ListEmptyComponent={
            <Text style={styles.emptyText}>No results found</Text>
          }
          contentContainerStyle={styles.listContent}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f8f9fa' },
  searchInput: {
    margin: 16,
    padding: 12,
    borderRadius: 8,
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#ddd',
    fontSize: 16,
  },
  card: {
    marginHorizontal: 16,
    marginBottom: 12,
    borderRadius: 12,
    backgroundColor: '#fff',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
    overflow: 'hidden',
  },
  cardPressed: { opacity: 0.9, transform: [{ scale: 0.98 }] },
  productImage: { width: '100%', height: 200 },
  cardContent: { padding: 16 },
  productName: { fontSize: 18, fontWeight: '600', color: '#333' },
  productPrice: { fontSize: 20, fontWeight: 'bold', color: '#007AFF', marginTop: 4 },
  productDescription: { fontSize: 14, color: '#666', marginTop: 8 },
  emptyText: { textAlign: 'center', fontSize: 16, color: '#999', marginTop: 32 },
  listContent: { paddingBottom: 20 },
});

export default ProductListScreen;

FlatList is particularly important for performance - unlike ScrollView, it only renders visible list items (virtualization), which significantly reduces memory usage for long lists.

React Navigation is the standard library for handling navigation in React Native. It supports stack navigation, bottom tabs, drawer navigation, and nesting navigators.

// npm install @react-navigation/native @react-navigation/native-stack
// npm install @react-navigation/bottom-tabs
// npm install react-native-screens react-native-safe-area-context

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, Pressable, StyleSheet } from 'react-native';

// Navigation parameter typing
type RootStackParamList = {
  HomeTabs: undefined;
  ProductDetail: { productId: string; productName: string };
  Cart: undefined;
};

type HomeTabParamList = {
  Products: undefined;
  Favorites: undefined;
  Profile: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<HomeTabParamList>();

// Products screen
import type { NativeStackScreenProps } from '@react-navigation/native-stack';

type ProductsScreenProps = NativeStackScreenProps<RootStackParamList, 'HomeTabs'>;

const ProductsScreen: React.FC<ProductsScreenProps> = ({ navigation }) => (
  <View style={styles.screen}>
    <Text style={styles.title}>Products</Text>
    <Pressable
      style={styles.button}
      onPress={() =>
        navigation.navigate('ProductDetail', {
          productId: '123',
          productName: 'ProBook Laptop',
        })
      }
    >
      <Text style={styles.buttonText}>View product details</Text>
    </Pressable>
  </View>
);

// Product detail screen
type DetailScreenProps = NativeStackScreenProps<RootStackParamList, 'ProductDetail'>;

const ProductDetailScreen: React.FC<DetailScreenProps> = ({ route, navigation }) => {
  const { productId, productName } = route.params;

  return (
    <View style={styles.screen}>
      <Text style={styles.title}>{productName}</Text>
      <Text>ID: {productId}</Text>
      <Pressable
        style={styles.button}
        onPress={() => navigation.navigate('Cart')}
      >
        <Text style={styles.buttonText}>Add to cart</Text>
      </Pressable>
    </View>
  );
};

// Tab navigator
const HomeTabs: React.FC = () => (
  <Tab.Navigator
    screenOptions={{
      tabBarActiveTintColor: '#007AFF',
      tabBarInactiveTintColor: '#999',
    }}
  >
    <Tab.Screen name="Products" component={ProductsScreen as any} />
    <Tab.Screen name="Favorites" component={FavoritesScreen} />
    <Tab.Screen name="Profile" component={ProfileScreen} />
  </Tab.Navigator>
);

// Main navigator
const App: React.FC = () => (
  <NavigationContainer>
    <Stack.Navigator
      screenOptions={{
        headerStyle: { backgroundColor: '#007AFF' },
        headerTintColor: '#fff',
        headerTitleStyle: { fontWeight: 'bold' },
      }}
    >
      <Stack.Screen
        name="HomeTabs"
        component={HomeTabs}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="ProductDetail"
        component={ProductDetailScreen}
        options={({ route }) => ({ title: route.params.productName })}
      />
      <Stack.Screen name="Cart" component={CartScreen} />
    </Stack.Navigator>
  </NavigationContainer>
);

Typing navigation parameters with NativeStackScreenProps ensures full type safety - your IDE will suggest available screens and required parameters with every navigation.navigate() call.

State Management#

In React Native, you can use the same state management solutions as in web React. The most popular options are Zustand, Redux Toolkit, Jotai, and the built-in Context API.

Zustand - Lightweight and Elegant#

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
  totalItems: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],

      addItem: (item) =>
        set((state) => {
          const existing = state.items.find((i) => i.id === item.id);
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
              ),
            };
          }
          return { items: [...state.items, { ...item, quantity: 1 }] };
        }),

      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((i) => i.id !== id),
        })),

      updateQuantity: (id, quantity) =>
        set((state) => ({
          items: state.items.map((i) =>
            i.id === id ? { ...i, quantity: Math.max(0, quantity) } : i
          ).filter((i) => i.quantity > 0),
        })),

      clearCart: () => set({ items: [] }),

      totalPrice: () =>
        get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),

      totalItems: () =>
        get().items.reduce((sum, item) => sum + item.quantity, 0),
    }),
    {
      name: 'cart-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// Usage in a component
const CartBadge: React.FC = () => {
  const totalItems = useCartStore((state) => state.totalItems());

  return totalItems > 0 ? (
    <View style={badgeStyles.badge}>
      <Text style={badgeStyles.badgeText}>{totalItems}</Text>
    </View>
  ) : null;
};

Zustand combined with AsyncStorage and the persist middleware provides automatic state persistence between app launches. This is especially useful for shopping carts, user preferences, and authentication tokens.

Native Modules#

Sometimes you need access to platform-specific native APIs that are not available in JavaScript. React Native enables creating Native Modules and Native Components.

// Example of using a native module - Biometrics
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';

const biometrics = new ReactNativeBiometrics();

interface AuthResult {
  success: boolean;
  error?: string;
}

const authenticateWithBiometrics = async (): Promise<AuthResult> => {
  try {
    const { available, biometryType } = await biometrics.isSensorAvailable();

    if (!available) {
      return { success: false, error: 'Biometrics not available on this device' };
    }

    const promptMessage =
      biometryType === BiometryTypes.FaceID
        ? 'Confirm your identity with Face ID'
        : 'Confirm your identity with fingerprint';

    const { success } = await biometrics.simplePrompt({
      promptMessage,
      cancelButtonText: 'Cancel',
    });

    return { success };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
};

// Creating a custom TurboModule (New Architecture)
// specs/NativeDeviceInfo.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getDeviceId(): string;
  getBatteryLevel(): Promise<number>;
  getAvailableStorage(): Promise<number>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');

In React Native's new architecture, TurboModules replace traditional Native Modules, offering lazy loading, direct communication with native code through JSI (JavaScript Interface), and typing through CodeGen.

The Hermes Engine#

Hermes is a JavaScript engine created by Meta specifically for React Native. Since version 0.70, it has been the default engine on both Android and iOS. Hermes compiles JavaScript to bytecode during the build process (AOT - Ahead of Time), which delivers measurable benefits.

The main advantages of Hermes include significantly faster app startup time (TTI - Time to Interactive), lower memory consumption, smaller app size, and better debugging support with Chrome DevTools and Flipper.

// Check if Hermes is active
const isHermesEnabled = (): boolean => {
  return !!(global as any).HermesInternal;
};

// Hermes supports modern JS syntax
// but has limited support for some APIs

// Intl support requires additional packages
// npm install @formatjs/intl-pluralrules
// npm install @formatjs/intl-numberformat

import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-numberformat/polyfill';
import '@formatjs/intl-numberformat/locale-data/en';

const formatPrice = (price: number): string => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(price);
};

New Architecture - Fabric and TurboModules#

The React Native New Architecture is a fundamental rebuild of the framework that introduces two key elements: Fabric (a new rendering system) and TurboModules (a new native module system). Since React Native 0.76, the new architecture is enabled by default.

Fabric Renderer#

Fabric replaces the old rendering system based on the asynchronous Bridge. Key changes include synchronous access to the native layer through JSI (JavaScript Interface), multi-threaded rendering with UI update prioritization, direct Shadow Tree creation in C++, and support for Concurrent Features from React 18.

JSI - JavaScript Interface#

// JSI eliminates the Bridge and enables direct communication
// JavaScript <-> C++ <-> Native (iOS/Android)

// Old architecture (Bridge):
// JS -> JSON serialize -> Bridge (async) -> JSON deserialize -> Native
// Latency: ~5-10ms per call

// New architecture (JSI):
// JS -> JSI (sync, direct call) -> C++ -> Native
// Latency: <1ms, synchronous calls

// Enabling the new architecture in react-native.config.js
module.exports = {
  project: {
    android: {
      unstable_reactLegacyComponentNames: [],
    },
    ios: {
      unstable_reactLegacyComponentNames: [],
    },
  },
};

The new architecture brings significant improvements in performance, interactivity, and application responsiveness. Eliminating the Bridge reduces communication latency between the JavaScript and native layers by an order of magnitude.

Performance Optimization#

Performance is critical in mobile applications. React Native offers many optimization techniques to achieve smooth operation at 60 FPS.

import React, { useCallback, useMemo, memo } from 'react';
import { FlatList, View, Text, StyleSheet, Pressable } from 'react-native';

// 1. Component memoization with React.memo
interface ListItemProps {
  id: string;
  title: string;
  onPress: (id: string) => void;
}

const ListItem = memo<ListItemProps>(({ id, title, onPress }) => {
  const handlePress = useCallback(() => {
    onPress(id);
  }, [id, onPress]);

  return (
    <Pressable onPress={handlePress} style={styles.listItem}>
      <Text style={styles.itemTitle}>{title}</Text>
    </Pressable>
  );
});

// 2. FlatList optimization
interface DataItem {
  id: string;
  title: string;
  category: string;
}

const OptimizedList: React.FC<{ data: DataItem[] }> = ({ data }) => {
  const renderItem = useCallback(
    ({ item }: { item: DataItem }) => (
      <ListItem id={item.id} title={item.title} onPress={handleItemPress} />
    ),
    []
  );

  const keyExtractor = useCallback((item: DataItem) => item.id, []);

  const getItemLayout = useCallback(
    (_: any, index: number) => ({
      length: ITEM_HEIGHT,
      offset: ITEM_HEIGHT * index,
      index,
    }),
    []
  );

  const handleItemPress = useCallback((id: string) => {
    console.log('Pressed:', id);
  }, []);

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      // Performance parameters
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={5}
      initialNumToRender={10}
      updateCellsBatchingPeriod={50}
    />
  );
};

const ITEM_HEIGHT = 60;

// 3. Image optimization with react-native-fast-image
import FastImage from 'react-native-fast-image';

const OptimizedImage: React.FC<{ uri: string }> = ({ uri }) => (
  <FastImage
    source={{
      uri,
      priority: FastImage.priority.normal,
      cache: FastImage.cacheControl.immutable,
    }}
    style={{ width: 200, height: 200 }}
    resizeMode={FastImage.resizeMode.cover}
  />
);

// 4. Animations with React Native Reanimated (on UI thread)
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolate,
} from 'react-native-reanimated';

const AnimatedCard: React.FC = () => {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    opacity: interpolate(scale.value, [0.95, 1], [0.8, 1]),
  }));

  const handlePressIn = () => {
    scale.value = withSpring(0.95, { damping: 15, stiffness: 150 });
  };

  const handlePressOut = () => {
    scale.value = withSpring(1, { damping: 15, stiffness: 150 });
  };

  return (
    <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Animated Card</Text>
      </Animated.View>
    </Pressable>
  );
};

Key performance principles include avoiding anonymous functions in renders, using useCallback and useMemo, using react-native-reanimated instead of the Animated API for complex animations, enabling removeClippedSubviews on long lists, and optimizing images through caching and appropriate sizes.

Testing#

Testing React Native applications involves unit tests, integration tests, and end-to-end (E2E) tests. The most commonly used tools are Jest, React Native Testing Library, and Detox.

// __tests__/CartStore.test.ts
import { useCartStore } from '../store/cartStore';
import { act, renderHook } from '@testing-library/react-hooks';

describe('CartStore', () => {
  beforeEach(() => {
    // Reset store before each test
    useCartStore.setState({ items: [] });
  });

  it('should add a product to the cart', () => {
    const { result } = renderHook(() => useCartStore());

    act(() => {
      result.current.addItem({
        id: '1',
        name: 'Laptop',
        price: 1299,
      });
    });

    expect(result.current.items).toHaveLength(1);
    expect(result.current.items[0].quantity).toBe(1);
  });

  it('should increase quantity for existing product', () => {
    const { result } = renderHook(() => useCartStore());

    act(() => {
      result.current.addItem({ id: '1', name: 'Laptop', price: 1299 });
      result.current.addItem({ id: '1', name: 'Laptop', price: 1299 });
    });

    expect(result.current.items).toHaveLength(1);
    expect(result.current.items[0].quantity).toBe(2);
  });

  it('should correctly calculate total price', () => {
    const { result } = renderHook(() => useCartStore());

    act(() => {
      result.current.addItem({ id: '1', name: 'Laptop', price: 1299 });
      result.current.addItem({ id: '2', name: 'Phone', price: 899 });
    });

    expect(result.current.totalPrice()).toBe(2198);
  });
});

// __tests__/ProductCard.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react-native';
import { ProductCard } from '../components/ProductCard';

describe('ProductCard', () => {
  const mockProduct = {
    id: '1',
    name: 'Test Product',
    price: 99,
    imageUrl: 'https://example.com/image.jpg',
    description: 'Test description',
  };

  it('should display product name and price', () => {
    const mockOnPress = jest.fn();

    render(<ProductCard product={mockProduct} onPress={mockOnPress} />);

    expect(screen.getByText('Test Product')).toBeTruthy();
    expect(screen.getByText('$99')).toBeTruthy();
  });

  it('should call onPress when tapped', () => {
    const mockOnPress = jest.fn();

    render(<ProductCard product={mockProduct} onPress={mockOnPress} />);

    fireEvent.press(screen.getByText('Test Product'));

    expect(mockOnPress).toHaveBeenCalledWith(mockProduct);
  });
});

// e2e/product.e2e.ts (Detox)
describe('Shopping flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should add product to cart and navigate to summary', async () => {
    await element(by.id('product-list')).scrollTo('bottom');
    await element(by.id('product-card-1')).tap();
    await element(by.id('add-to-cart-button')).tap();

    await expect(element(by.id('cart-badge'))).toHaveText('1');

    await element(by.id('cart-icon')).tap();
    await expect(element(by.id('cart-total'))).toBeVisible();
  });
});

Good test coverage is crucial for mobile app stability, as every update must go through the app store review process, which can take from one to several days.

Deploying to App Store and Google Play#

The process of publishing applications to mobile platforms requires several configuration steps and meeting the requirements of each store.

// app.json (Expo) - deployment configuration
{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "bundleIdentifier": "com.mycompany.myapp",
      "buildNumber": "1",
      "supportsTablet": true,
      "infoPlist": {
        "NSCameraUsageDescription": "The app requires access to the camera",
        "NSLocationWhenInUseUsageDescription": "We need your location to find nearby stores"
      }
    },
    "android": {
      "package": "com.mycompany.myapp",
      "versionCode": 1,
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
    }
  }
}

Building and Publishing with EAS (Expo Application Services)#

# Install EAS CLI
npm install -g eas-cli

# Build profile configuration
# eas.json
# {
#   "build": {
#     "development": {
#       "developmentClient": true,
#       "distribution": "internal"
#     },
#     "preview": {
#       "distribution": "internal",
#       "ios": { "simulator": true }
#     },
#     "production": {
#       "autoIncrement": true
#     }
#   },
#   "submit": {
#     "production": {
#       "ios": { "appleId": "dev@company.com", "ascAppId": "123456789" },
#       "android": { "serviceAccountKeyPath": "./google-play-key.json" }
#     }
#   }
# }

# Build for production
eas build --platform all --profile production

# Automatic submission to stores
eas submit --platform ios --profile production
eas submit --platform android --profile production

# OTA (Over-the-Air) updates without going through review
eas update --branch production --message "Fix price display issue"

EAS Update enables publishing JavaScript updates without going through the store review process again. This is a powerful tool for quickly fixing bugs and deploying minor changes.

React Native vs Flutter#

Comparing React Native with Flutter is one of the most common questions when choosing a mobile framework. Both tools have their strengths.

React Native uses JavaScript/TypeScript, renders native platform components, leverages the massive npm ecosystem, and is a natural choice for teams with React experience. The community is very large, and Meta actively supports the project.

Flutter uses Dart, has its own rendering engine (Skia/Impeller), offers a rich library of Material and Cupertino widgets, and generates compiled native code. Flutter provides a more consistent look across platforms at the cost of less "native-feeling" UI.

| Aspect | React Native | Flutter | |--------|-------------|---------| | Language | TypeScript/JavaScript | Dart | | UI | Native platform components | Custom widgets | | Performance | Excellent (New Architecture) | Excellent (AOT compilation) | | Ecosystem | Massive (npm) | Growing (pub.dev) | | Hot Reload | Yes | Yes | | App Size | ~7-15 MB | ~10-20 MB | | Learning Curve | Easy (for React developers) | Medium (new language) | | Web Support | React Native Web (limited) | Flutter Web (official) |

The choice between React Native and Flutter depends mainly on the team's experience, project requirements, and priorities regarding the app's look and feel. If the team already knows React, React Native is the natural choice. If a priority is an identical look on both platforms, Flutter may be a better solution.

Ecosystem and Useful Libraries#

React Native has a rich library ecosystem that accelerates development. Here are the most important ones organized by category.

For navigation, the standard is React Navigation or Expo Router (file-based, inspired by Next.js). For state management, popular choices are Zustand, Redux Toolkit, or TanStack Query (formerly React Query) for server data. For forms, React Hook Form with Zod for validation works well. Animations are best handled with React Native Reanimated and React Native Gesture Handler. For styling, consider NativeWind (Tailwind CSS for React Native) or Tamagui. For local data storage, MMKV (faster than AsyncStorage) or WatermelonDB (relational database) are excellent choices. For networking, Axios or the native fetch with TanStack Query are the most popular options.

Summary#

React Native is a mature and proven framework for building mobile applications that enables creating native apps for iOS and Android from a single TypeScript codebase. The new architecture with Fabric and TurboModules, the Hermes engine, and the growing ecosystem of tools such as Expo make React Native an excellent choice for both startups and large enterprises.

Key success factors in React Native projects include choosing the right architecture (Expo vs bare workflow), implementing good state management practices, optimizing performance from the start, and establishing a solid testing strategy. With a single codebase, you save time and resources while the application delivers a native user experience on both platforms.


Need a professional mobile application? The MDS Software Solutions Group team specializes in building high-performance mobile applications with React Native. From prototype to store publication - we will guide your project through the entire development process. Contact us and let's discuss your project!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

React Native - Building Mobile Applications in 2025 | MDS Software Solutions Group | MDS Software Solutions Group