Przejdź do treści
Mobile

React Native - Tworzenie aplikacji mobilnych w 2025

Opublikowano:
·7 min czytania·Autor: MDS Software Solutions Group

React Native Tworzenie

mobile

React Native - Tworzenie aplikacji mobilnych

React Native to jeden z najpopularniejszych frameworków do tworzenia aplikacji mobilnych na platformy iOS i Android z wykorzystaniem jednej bazy kodu napisanej w JavaScript i TypeScript. Stworzony przez Meta (dawniej Facebook) w 2015 roku, framework ten zrewolucjonizował podejście do developmentu mobilnego, umożliwiając webowym deweloperom budowanie natywnych aplikacji bez konieczności nauki Swift/Kotlin od podstaw. W tym artykule szczegółowo omówimy architekturę React Native, narzędzia, wzorce projektowe i najlepsze praktyki produkcyjne.

Czym jest React Native?#

React Native to framework open-source, który pozwala budować prawdziwie natywne aplikacje mobilne przy użyciu React. W przeciwieństwie do rozwiązań hybrydowych opartych na WebView (jak Cordova/PhoneGap), React Native renderuje natywne komponenty UI specyficzne dla danej platformy. Oznacza to, że przycisk w React Native to prawdziwy UIButton na iOS i android.widget.Button na Androidzie.

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}>
        Witaj w React Native!
      </Text>
      <Text style={styles.subtitle}>
        Platforma: {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;

Kluczowe cechy React Native to dzielenie kodu miedzy platformami (typowo 80-95% kodu jest wspólne), hot reloading umozliwiajacy natychmiastowe podgladanie zmian, bogaty ekosystem bibliotek oraz duza i aktywna spolecznosc deweloperow.

Expo vs Bare Workflow#

Rozpoczynajac prace z React Native, pierwsza decyzja dotyczy wyboru miedzy Expo a bare workflow. Kazde podejscie ma swoje zalety i ograniczenia.

Expo - szybki start#

Expo to zestaw narzedzi i uslug zbudowanych wokol React Native, ktory znacznie upraszcza proces developmentu. Expo SDK udostepnia gotowe API do kamery, lokalizacji, powiadomien push i wielu innych funkcji bez koniecznosci konfiguracji natywnego kodu.

// Inicjalizacja projektu Expo
// npx create-expo-app@latest MojaAplikacja --template blank-typescript

import { StatusBar } from 'expo-status-bar';
import * as Location from 'expo-location';
import * as Camera from 'expo-camera';
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('Brak uprawnien', 'Aplikacja wymaga dostepu do lokalizacji');
        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>
          Lokalizacja: {location.latitude.toFixed(4)}, {location.longitude.toFixed(4)}
        </Text>
      ) : (
        <Text>Pobieranie lokalizacji...</Text>
      )}
      <StatusBar style="auto" />
    </View>
  );
};

Od Expo SDK 49+ dostepny jest rowniez Expo Dev Client, ktory pozwala na uzycie natywnych modulow niestandardowych przy zachowaniu wygody Expo. Dzieki Continuous Native Generation (CNG) Expo generuje natywne projekty iOS i Android automatycznie na podstawie konfiguracji w app.json.

Bare Workflow#

Bare workflow daje pelna kontrole nad natywnym kodem iOS (Xcode) i Android (Android Studio). Jest to preferowane podejscie, gdy aplikacja wymaga zaawansowanej integracji z natywnymi SDK, niestandardowych modulow natywnych lub specyficznych konfiguracji buildow.

# Inicjalizacja bare workflow
npx react-native@latest init MojaAplikacja --template react-native-template-typescript

# Struktura projektu
# ├── android/          <- natywny kod Android
# ├── ios/              <- natywny kod iOS
# ├── src/
# │   ├── components/
# │   ├── screens/
# │   ├── navigation/
# │   ├── store/
# │   ├── hooks/
# │   ├── services/
# │   └── types/
# ├── App.tsx
# ├── tsconfig.json
# └── package.json

W praktyce Expo z Dev Clientem pokrywa wiekszosc przypadkow uzycia i jest rekomendowanym podejsciem dla nowych projektow. Bare workflow warto rozwazyc jedynie w przypadku bardzo specyficznych wymagan natywnych.

Komponenty React Native#

React Native dostarcza zestaw podstawowych komponentow, ktore mapuja sie na natywne widoki. Najwazniejsze z nich to View, Text, Image, ScrollView, FlatList, TextInput, TouchableOpacity i 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: 'Laptop ProBook',
    price: 4999,
    imageUrl: 'https://example.com/laptop.jpg',
    description: 'Wydajny laptop do pracy',
  },
  {
    id: '2',
    name: 'Smartfon Ultra',
    price: 2999,
    imageUrl: 'https://example.com/phone.jpg',
    description: 'Flagowy smartfon z swietnym aparatem',
  },
];

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} PLN</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('Wybrano produkt:', product.name);
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.searchInput}
        placeholder="Szukaj produktow..."
        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}>Brak wynikow</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 jest szczegolnie wazny dla wydajnosci - w przeciwienstwie do ScrollView, renderuje tylko widoczne elementy listy (wirtualizacja), co znaczaco zmniejsza zuzycie pamieci przy dlugich listach.

Nawigacja z React Navigation#

React Navigation to standardowa biblioteka do obslugi nawigacji w React Native. Wspiera nawigacje stosowa (stack), dolna zakladkowa (bottom tabs), szufladowa (drawer) oraz zagniezdzanie nawigatorow.

// 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';

// Typowanie parametrow nawigacji
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>();

// Ekran listy produktow
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}>Produkty</Text>
    <Pressable
      style={styles.button}
      onPress={() =>
        navigation.navigate('ProductDetail', {
          productId: '123',
          productName: 'Laptop ProBook',
        })
      }
    >
      <Text style={styles.buttonText}>Zobacz szczegoly produktu</Text>
    </Pressable>
  </View>
);

// Ekran szczegulow produktu
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}>Dodaj do koszyka</Text>
      </Pressable>
    </View>
  );
};

// Nawigator zakladkowy
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>
);

// Glowny nawigator
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>
);

Typowanie parametrow nawigacji z NativeStackScreenProps zapewnia pelne bezpieczenstwo typow - IDE podpowie dostepne ekrany i wymagane parametry przy kazdym wywolaniu navigation.navigate().

Zarzadzanie stanem#

W React Native mozna korzystac z tych samych rozwiazan do zarzadzania stanem co w React webowym. Najpopularniejsze opcje to Zustand, Redux Toolkit, Jotai oraz wbudowany Context API.

Zustand - lekkie i eleganckie rozwiazanie#

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),
    }
  )
);

// Uzycie w komponencie
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 w polaczeniu z AsyncStorage i middleware persist zapewnia automatyczna persystencje stanu miedzy uruchomieniami aplikacji. Jest to szczegolnie przydatne dla koszyka zakupowego, preferencji uzytkownika czy tokenow uwierzytelniania.

Moduly natywne#

Czasami potrzebujesz dostepu do natywnych API platformy, ktore nie sa dostepne w JavaScript. React Native umozliwia tworzenie modulow natywnych (Native Modules) oraz komponentow natywnych (Native Components).

// Przyklad uzycia modulu natywnego - Biometria
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: 'Biometria niedostepna na tym urzadzeniu' };
    }

    const promptMessage =
      biometryType === BiometryTypes.FaceID
        ? 'Potwierdz tozsamosc za pomoca Face ID'
        : 'Potwierdz tozsamosc za pomoca odcisku palca';

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

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

// Tworzenie wlasnego 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');

W nowej architekturze React Native, TurboModules zastepuja tradycyjne Native Modules, oferujac leniwe ladowanie (lazy loading), bezposrednia komunikacje z kodem natywnym przez JSI (JavaScript Interface) oraz typowanie za pomoca CodeGen.

Silnik Hermes#

Hermes to silnik JavaScript stworzony przez Meta specjalnie dla React Native. Od wersji 0.70 jest domyslnym silnikiem zarowno na Androidzie, jak i iOS. Hermes kompiluje JavaScript do bytecode'u podczas budowania aplikacji (AOT - Ahead of Time), co przynosi wymierne korzysci.

Glowne zalety Hermes to znacznie szybszy czas uruchamiania aplikacji (TTI - Time to Interactive), mniejsze zuzycie pamieci RAM, mniejszy rozmiar aplikacji oraz lepsze wsparcie dla debugowania z Chrome DevTools i Flipper.

// Sprawdzenie czy Hermes jest aktywny
const isHermesEnabled = (): boolean => {
  return !!(global as any).HermesInternal;
};

// Hermes wspiera nowoczesna skladnie JS
// ale ma ograniczone wsparcie dla niektorych API

// Wsparcie Intl wymaga dodatkowych pakietow
// 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/pl';

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

New Architecture - Fabric i TurboModules#

React Native New Architecture to fundamentalna przebudowa frameworka, ktora wprowadza dwa kluczowe elementy: Fabric (nowy system renderowania) i TurboModules (nowy system modulow natywnych). Od React Native 0.76 nowa architektura jest domyslnie wlaczona.

Fabric Renderer#

Fabric zastepuje stary system renderowania oparty na Bridge asynchronicznym. Kluczowe zmiany to synchroniczny dostep do warstwy natywnej przez JSI (JavaScript Interface), renderowanie wielowatkowe z mozliwoscia priorytetyzacji aktualizacji UI, bezposrednie tworzenie Shadow Tree w C++ oraz wsparcie dla Concurrent Features z React 18.

JSI - JavaScript Interface#

// JSI eliminuje Bridge i umozliwia bezposrednia komunikacje
// JavaScript <-> C++ <-> Native (iOS/Android)

// Stara architektura (Bridge):
// JS -> JSON serialize -> Bridge (async) -> JSON deserialize -> Native
// Latencja: ~5-10ms na kazde wywolanie

// Nowa architektura (JSI):
// JS -> JSI (sync, direct call) -> C++ -> Native
// Latencja: <1ms, synchroniczne wywolania

// Wlaczenie nowej architektury w react-native.config.js
module.exports = {
  project: {
    android: {
      unstable_reactLegacyComponentNames: [],
    },
    ios: {
      unstable_reactLegacyComponentNames: [],
    },
  },
};

Nowa architektura przynosi znaczace usprawnienia w zakresie wydajnosci, interaktywnosci i responsywnosci aplikacji. Eliminacja Bridge redukuje latencje komunikacji miedzy warstwami JavaScript i natywna o rzad wielkosci.

Optymalizacja wydajnosci#

Wydajnosc jest krytyczna w aplikacjach mobilnych. React Native oferuje wiele technik optymalizacji, ktore pozwalaja osiagnac plynne dzialanie na poziomie 60 FPS.

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

// 1. Memoizacja komponentow z 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. Optymalizacja FlatList
interface DataItem {
  id: string;
  title: string;
  category: string;
}

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

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

  // Optymalizacja getItemLayout dla stalej wysokosci elementow
  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}
      // Parametry wydajnosci
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={5}
      initialNumToRender={10}
      updateCellsBatchingPeriod={50}
    />
  );
};

const ITEM_HEIGHT = 60;

// 3. Optymalizacja obrazow z 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. Animacje z React Native Reanimated (na watku UI)
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  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>Animowana karta</Text>
      </Animated.View>
    </Pressable>
  );
};

Kluczowe zasady wydajnosci to unikanie anonimowych funkcji w renderze, korzystanie z useCallback i useMemo, uzycie react-native-reanimated zamiast Animated API dla zlozonych animacji, wlaczenie removeClippedSubviews na dlugich listach oraz optymalizacja obrazow przez cache i odpowiednie rozmiary.

Testowanie#

Testowanie aplikacji React Native obejmuje testy jednostkowe, integracyjne i end-to-end (E2E). Najczesciej stosowane narzedzia to Jest, React Native Testing Library oraz Detox.

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

describe('CartStore', () => {
  beforeEach(() => {
    // Reset store przed kazdym testem
    useCartStore.setState({ items: [] });
  });

  it('powinien dodac produkt do koszyka', () => {
    const { result } = renderHook(() => useCartStore());

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

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

  it('powinien zwiekszyc ilosc istniejacego produktu', () => {
    const { result } = renderHook(() => useCartStore());

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

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

  it('powinien poprawnie obliczyc laczna cene', () => {
    const { result } = renderHook(() => useCartStore());

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

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

// __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: 'Opis testowy',
  };

  it('powinien wyswietlic nazwe i cene produktu', () => {
    const mockOnPress = jest.fn();

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

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

  it('powinien wywolac onPress po kliknieciu', () => {
    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('Przeplyw zakupowy', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('powinien dodac produkt do koszyka i przejsc do podsumowania', 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();
  });
});

Dobre pokrycie testami jest kluczowe dla stabilnosci aplikacji mobilnych, poniewaz kazda aktualizacja musi przejsc przez proces review w sklepach z aplikacjami, co moze trwac od jednego do kilku dni.

Wdrazanie na App Store i Google Play#

Proces publikacji aplikacji na platformy mobilne wymaga kilku krokow konfiguracyjnych i spelnienia wymagan poszczegolnych sklepow.

// app.json (Expo) - konfiguracja wdrazania
{
  "expo": {
    "name": "MojaAplikacja",
    "slug": "moja-aplikacja",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "bundleIdentifier": "com.mojafirma.mojaaplikacja",
      "buildNumber": "1",
      "supportsTablet": true,
      "infoPlist": {
        "NSCameraUsageDescription": "Aplikacja wymaga dostepu do kamery",
        "NSLocationWhenInUseUsageDescription": "Potrzebujemy lokalizacji do znalezienia najblizszych sklepow"
      }
    },
    "android": {
      "package": "com.mojafirma.mojaaplikacja",
      "versionCode": 1,
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
    }
  }
}

Budowanie i publikacja z EAS (Expo Application Services)#

# Instalacja EAS CLI
npm install -g eas-cli

# Konfiguracja profili budowania
# eas.json
# {
#   "build": {
#     "development": {
#       "developmentClient": true,
#       "distribution": "internal"
#     },
#     "preview": {
#       "distribution": "internal",
#       "ios": { "simulator": true }
#     },
#     "production": {
#       "autoIncrement": true
#     }
#   },
#   "submit": {
#     "production": {
#       "ios": { "appleId": "dev@firma.pl", "ascAppId": "123456789" },
#       "android": { "serviceAccountKeyPath": "./google-play-key.json" }
#     }
#   }
# }

# Budowanie na produkcje
eas build --platform all --profile production

# Automatyczne wyslanie do sklepow
eas submit --platform ios --profile production
eas submit --platform android --profile production

# OTA (Over-the-Air) aktualizacje bez przechodzenia przez review
eas update --branch production --message "Poprawka wyswietlania cen"

EAS Update umozliwia publikowanie aktualizacji JavaScript bez koniecznosci ponownego przechodzenia przez review w sklepach. Jest to potezne narzedzie do szybkiego naprawiania bledow i wdrazania drobnych zmian.

React Native vs Flutter#

Porownanie React Native z Flutterem to jedno z najczestszych pytan przy wyborze frameworka mobilnego. Oba narzedzia maja swoje mocne strony.

React Native uzywa JavaScript/TypeScript, renderuje natywne komponenty platformy, korzysta z ogromnego ekosystemu npm i jest naturalnym wyborem dla zespolow z doswiadczeniem w React. Spolecznosc jest bardzo duza, a wsparcie ze strony Meta aktywne.

Flutter uzywa Dart, posiada wlasny silnik renderowania (Skia/Impeller), oferuje bogata biblioteke widgetow Material i Cupertino oraz generuje kompilowany kod natywny. Flutter zapewnia bardziej spojny wyglad miedzy platformami kosztem mniejszej "natywnoscoi" UI.

| Aspekt | React Native | Flutter | |--------|-------------|---------| | Jezyk | TypeScript/JavaScript | Dart | | UI | Natywne komponenty platformy | Wlasne widgety | | Wydajnosc | Bardzo dobra (New Architecture) | Bardzo dobra (kompilacja AOT) | | Ekosystem | Ogromny (npm) | Rosnacy (pub.dev) | | Hot Reload | Tak | Tak | | Rozmiar aplikacji | ~7-15 MB | ~10-20 MB | | Krzywa uczenia | Latwa (dla deweloperow React) | Srednia (nowy jezyk) | | Web support | React Native Web (ograniczony) | Flutter Web (oficjalny) |

Wybor miedzy React Native a Flutterem zalezy glownie od doswiadczenia zespolu, wymagan projektu oraz priorytetow dotyczacych wygladu aplikacji. Jesli zespol juz zna React, React Native jest naturalnym wyborem. Jesli priorytetem jest identyczny wyglad na obu platformach, Flutter moze byc lepszym rozwiazaniem.

Ekosystem i przydatne biblioteki#

React Native posiada bogaty ekosystem bibliotek, ktore przyspieszaja development. Oto najwazniejsze z nich podzielone na kategorie.

Do nawigacji standardem jest React Navigation lub Expo Router (oparty na plikach, wzorowany na Next.js). Do zarzadzania stanem popularny jest Zustand, Redux Toolkit lub TanStack Query (dawniej React Query) do danych serwerowych. Do formularzy sprawdza sie React Hook Form z Zod do walidacji. Animacje najlepiej realizowac z React Native Reanimated i React Native Gesture Handler. Do stylowania warto rozwazyc NativeWind (Tailwind CSS dla React Native) lub Tamagui. Do przechowywania danych lokalnych sluzy MMKV (szybszy od AsyncStorage) lub WatermelonDB (relacyjna baza danych). Do komunikacji sieciowej najpopularniejszy jest Axios lub natywny fetch z TanStack Query.

Podsumowanie#

React Native to dojrzaly i sprawdzony framework do tworzenia aplikacji mobilnych, ktory umozliwia budowanie natywnych aplikacji na iOS i Android z jednej bazy kodu TypeScript. Nowa architektura z Fabric i TurboModules, silnik Hermes oraz rosnacy ekosystem narzedzi takich jak Expo sprawiaja, ze React Native jest doskonalym wyborem zarowno dla startupow, jak i duzych przedsiebiorstw.

Kluczowe czynniki sukcesu w projektach React Native to odpowiedni wybor architektury (Expo vs bare workflow), dobre praktyki zarzadzania stanem, optymalizacja wydajnosci od samego poczatku oraz solidna strategia testowania. Dzieki jednej bazie kodu oszczedzasz czas i zasoby, a aplikacja zapewnia natywne doswiadczenie uzytkownika na obu platformach.


Potrzebujesz profesjonalnej aplikacji mobilnej? Zespol MDS Software Solutions Group specjalizuje sie w tworzeniu wydajnych aplikacji mobilnych w React Native. Od prototypu po publikacje w sklepach - przeprowadzimy Twoj projekt przez caly proces developmentu. Skontaktuj sie z nami i porozmawiajmy o Twoim projekcie!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

React Native - Tworzenie aplikacji mobilnych w 2025 | MDS Software Solutions Group | MDS Software Solutions Group