React Native — Xây Dựng Mobile App Từ Một Codebase
React Native cơ bản: navigation, state management, native modules, performance optimization và deploy lên App Store.
React Native cho phép xây dựng iOS và Android từ một codebase JavaScript/TypeScript. Không phải “web view” — nó dùng native components thật.
React Native vs Flutter#
| React Native | Flutter | |
|---|---|---|
| Language | JavaScript/TypeScript | Dart |
| UI | Native components | Custom canvas (Skia) |
| Performance | Bridge (JSI mới) | Native (compiled) |
| Ecosystem | Lớn, nhiều thư viện | Đang phát triển |
| Hot reload | ✅ | ✅ |
| Bundle size | ~10MB | ~15MB |
Setup#
# Dùng Expo (khuyến nghị)
npx create-expo-app my-app --template blank-typescript
cd my-app
npx expo start
# Hoặc React Native CLI
npx @react-native-community/cli init MyAppbashComponents Cơ Bản#
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<View style={styles.header}>
<Text style={styles.title}>Hello, Mobile!</Text>
<Text style={styles.subtitle}>Welcome to React Native</Text>
</View>
<TouchableOpacity style={styles.button} onPress={() => alert('Pressed!')}>
<Text style={styles.buttonText}>Press Me</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
padding: 24,
alignItems: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#1a1a1a',
},
subtitle: {
fontSize: 16,
color: '#666',
marginTop: 8,
},
button: {
backgroundColor: '#007AFF',
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 12,
marginHorizontal: 24,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});tsxNavigation#
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
type RootStackParamList = {
Home: undefined;
Profile: { userId: number };
Settings: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator();
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
function ProfileScreen({ route, navigation }: any) {
const { userId } = route.params;
return (
<View>
<Text>User ID: {userId}</Text>
<Button title="Go Back" onPress={() => navigation.goBack()} />
</View>
);
}tsxState Management (Zustand)#
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthStore {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
user: null,
token: null,
login: async (email, password) => {
const response = await fetch('https://api.example.com/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
set({ user: data.user, token: data.token });
},
logout: () => {
set({ user: null, token: null });
},
}),
{
name: 'auth-storage',
storage: {
getItem: async (name) => {
const value = await AsyncStorage.getItem(name);
return value ? JSON.parse(value) : null;
},
setItem: async (name, value) => {
await AsyncStorage.setItem(name, JSON.stringify(value));
},
removeItem: async (name) => {
await AsyncStorage.removeItem(name);
},
},
}
),
);tsxNetwork Calls#
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json()),
});
const createMutation = useMutation({
mutationFn: (newPost) =>
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error loading posts</Text>;
return (
<FlatList
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.post}>
<Text style={styles.postTitle}>{item.title}</Text>
<Text>{item.body}</Text>
</View>
)}
refreshing={isLoading}
onRefresh={() => queryClient.invalidateQueries({ queryKey: ['posts'] })}
/>
);
}tsxNative Features#
import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
// Camera
async function pickImage() {
const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
allowsEditing: true,
});
if (!result.canceled) {
setImage(result.assets[0].uri);
}
}
// Location
async function getLocation() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
const location = await Location.getCurrentPositionAsync({});
console.log(location.coords.latitude, location.coords.longitude);
}
// Push Notifications
async function registerForPush() {
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
const token = await Notifications.getExpoPushTokenAsync();
console.log(token.data);
}tsxPerformance#
// 1. FlatList thay ScrollView
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={item => item.id}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
/>
// 2. useMemo + useCallback
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const handlePress = useCallback((id: number) => {
navigation.navigate('Detail', { id });
}, []);
// 3. Images
<Image
source={{ uri: imageUrl }}
style={styles.image}
resizeMode="cover"
fadeDuration={300}
/>
// 4. Avoid inline styles
const styles = StyleSheet.create({
container: { flex: 1 }, // Không tạo object mới mỗi render
});tsxDeploy#
# Android — APK/AAB
cd android
./gradlew assembleRelease
# File ở android/app/build/outputs/apk/release/
# iOS — Archive
# Mở .xcworkspace trong Xcode → Product → Archive
# EAS Build (Expo)
eas build --platform all --profile production
eas submit --platform ios
eas submit --platform androidbashExpo vs React Native CLI#
| Expo | React Native CLI |
|---|---|
| ✅ Dễ setup, không cần Xcode/Android Studio | Cần native environment |
| ✅ OTA updates | Phải build lại |
| ✅ 50+ prebuilt modules | Tự config native |
| ❌ Không dùng native module tùy chỉnh | ✅ Full native control |
| ❌ Bundle size lớn hơn | ✅ Tối ưu hơn |
Khuyến nghị: Bắt đầu với Expo. Chỉ chuyển sang bare workflow khi cần native module đặc biệt.
Kết Luận#
React Native là lựa chọn tốt cho mobile app nếu bạn đã biết React:
- Một codebase cho iOS + Android
- Hot reload
- Ecosystem lớn (Expo, React Navigation, TanStack Query)
- Có thể fallback sang native code khi cần
Bắt đầu với Expo + TypeScript, thêm navigation với React Navigation, state với Zustand + TanStack Query, deploy với EAS Build.