Bắt đầu tại reactnavigation.org/docs/getting-started. Đọc qua các phần: Getting Started, Hello React Navigation, Moving between screens, Passing parameters. Mục tiêu: hiểu flow cơ bản của navigation.
Tạo project mới, cài đặt React Navigation. Xây dựng cấu trúc: Bottom Tab Navigator chứa các Stack Navigator bên trong. Thực hành tạo ít nhất 3 tab, mỗi tab có 2-3 screen trong stack riêng.
Đọc Type checking with TypeScript. Tạo RootStackParamList, khai báo params cho mỗi screen. Thử navigate với params đúng/sai type để thấy TypeScript bắt lỗi.
Đọc Deep Linking docs. Cấu hình linking config cho app. Test bằng npx uri-scheme open hoặc adb shell am start. Sau đó thử Universal Links / App Links.
Navigation state trong React Navigation là một plain JavaScript object có cấu trúc cây. Mỗi navigator quản lý một phần state, các navigator lồng nhau tạo thành cây state phân cấp.
Cấu trúc cơ bản của navigation state:
{
routes: [
{
name: "Home",
key: "home-xxx",
params: undefined
},
{
name: "Profile",
key: "profile-xxx",
params: { userId: "42" }
}
],
index: 1,
type: "stack"
}
Trong đó index trỏ tới route đang active trong mảng routes. Mỗi navigator type (stack, tab, drawer) có thể thêm các field riêng vào state.
Setup cơ bản của NavigationContainer:
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
| Navigator | Package | Khi nào dùng | Đặc điểm |
|---|---|---|---|
| Native Stack | @react-navigation/native-stack | Navigation chính giữa các screen, auth flow, detail screen | Dùng native API (UINavigationController, Fragment), performance tốt nhất, animation native |
| JS Stack | @react-navigation/stack | Cần custom animation phức tạp, shared element transition | Render bằng JS/Animated, flexible hơn nhưng chậm hơn Native Stack |
| Bottom Tab | @react-navigation/bottom-tabs | Main navigation giữa các section chính (Home, Search, Profile) | Tab bar cố định ở dưới, giữ state mỗi tab, UX quen thuộc |
| Material Top Tab | @react-navigation/material-top-tabs | Tab ngang ở trên, swipe giữa các tab (như tab Categories) | Swipeable, scrollable tab bar, dùng react-native-tab-view |
| Drawer | @react-navigation/drawer | Side menu cho settings, profile, secondary navigation | Slide-in menu từ cạnh, thường dùng cho app có nhiều section |
@react-navigation/native-stack) cho performance. Chỉ dùng JS Stack khi cần custom transition animation mà Native Stack không hỗ trợ, hoặc cần shared element transition.
Pattern phổ biến nhất trong ứng dụng thực tế: Tab chứa Stack. Mỗi tab có stack navigator riêng, cho phép navigate sâu vào detail mà vẫn giữ tab bar.
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
const ProfileStack = createNativeStackNavigator();
function HomeStackScreen() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="HomeMain" component={HomeScreen} />
<HomeStack.Screen name="ProductDetail" component={ProductDetailScreen} />
</HomeStack.Navigator>
);
}
function ProfileStackScreen() {
return (
<ProfileStack.Navigator>
<ProfileStack.Screen name="ProfileMain" component={ProfileScreen} />
<ProfileStack.Screen name="EditProfile" component={EditProfileScreen} />
<ProfileStack.Screen name="Settings" component={SettingsScreen} />
</ProfileStack.Navigator>
);
}
function MainTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeStackScreen}
options={{ headerShown: false }}
/>
<Tab.Screen
name="Profile"
component={ProfileStackScreen}
options={{ headerShown: false }}
/>
</Tab.Navigator>
);
}
Khi nest Tab trong Stack, nhớ set headerShown: false cho tab screen để tránh double header (header của Stack + header của Tab).
Pattern cho modal full-screen: đặt modal screen trong group riêng với presentation: "modal".
function RootStack() {
return (
<Stack.Navigator>
<Stack.Group>
<Stack.Screen name="Main" component={MainTabs} options={{ headerShown: false }} />
</Stack.Group>
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="CreatePost" component={CreatePostScreen} />
<Stack.Screen name="ImagePicker" component={ImagePickerScreen} />
</Stack.Group>
</Stack.Navigator>
);
}
Modal screen sẽ slide lên từ dưới (iOS) hoặc fade in (Android), và có thể dismiss bằng swipe down.
Screen options cho phép cấu hình header, animation, gesture cho từng screen hoặc toàn bộ navigator.
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
title: "Hồ sơ",
headerShown: true,
headerStyle: { backgroundColor: "#1a1d27" },
headerTintColor: "#fff",
headerTitleStyle: { fontWeight: "bold" },
headerRight: () => (
<TouchableOpacity onPress={handleSettings}>
<Icon name="settings" size={24} color="#fff" />
</TouchableOpacity>
),
}}
/>
Có thể set options động từ bên trong screen bằng navigation.setOptions():
function ProfileScreen({ navigation, route }) {
React.useLayoutEffect(() => {
navigation.setOptions({
title: route.params?.userName ?? "Profile",
});
}, [navigation, route.params?.userName]);
return <View>...</View>;
}
<Stack.Navigator
screenOptions={{
header: ({ navigation, route, options }) => (
<SafeAreaView style={{ backgroundColor: "#1a1d27" }}>
<View style={{ flexDirection: "row", alignItems: "center", height: 56, paddingHorizontal: 16 }}>
{navigation.canGoBack() && (
<TouchableOpacity onPress={navigation.goBack}>
<Icon name="arrow-back" size={24} color="#fff" />
</TouchableOpacity>
)}
<Text style={{ color: "#fff", fontSize: 18, fontWeight: "bold", marginLeft: 12 }}>
{options.title ?? route.name}
</Text>
</View>
</SafeAreaView>
),
}}
>
| Action | Hành vi | Use Case |
|---|---|---|
navigate("Screen") |
Nếu screen đã có trong stack → quay về đó (không tạo mới). Nếu chưa → push mới. | Hầu hết mọi trường hợp điều hướng thông thường |
push("Screen") |
Luôn tạo instance mới trên stack, kể cả khi screen đã tồn tại | Chat detail: mở nhiều conversation cùng lúc, product → related product |
replace("Screen") |
Thay thế screen hiện tại bằng screen mới, không thêm vào stack | Login → Home (không cho quay lại Login) |
reset({ routes: [...] }) |
Xóa toàn bộ stack cũ, thay bằng state mới | Logout: xóa toàn bộ history, đưa về Login screen |
goBack() |
Quay lại screen trước đó trong stack | Back button, dismiss modal |
popToTop() |
Quay về screen đầu tiên trong stack, pop hết tất cả screen khác | Nút "Home" trong nested stack sâu |
navigate("B") → stack thành [A, B] (quay về B cũ). Gọi push("B") → stack thành [A, B, C, B] (tạo B mới). Đây là khác biệt quan trọng nhất cần nhớ.
Để truyền dữ liệu giữa các screen, dùng params:
navigation.navigate("ProductDetail", { productId: "abc123", title: "iPhone 15" });
Nhận params trong screen đích:
function ProductDetailScreen({ route }) {
const { productId, title } = route.params;
return <Text>{title} - {productId}</Text>;
}
Khai báo ParamList — một type map giữa tên screen và params tương ứng:
type RootStackParamList = {
Home: undefined;
ProductDetail: { productId: string; title: string };
Cart: { couponCode?: string };
Search: { query: string; category?: string };
};
Tạo typed navigator:
import { NativeStackScreenProps } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator<RootStackParamList>();
Typed screen props:
type ProductDetailProps = NativeStackScreenProps<RootStackParamList, "ProductDetail">;
function ProductDetailScreen({ route, navigation }: ProductDetailProps) {
const { productId, title } = route.params;
const goToCart = () => {
navigation.navigate("Cart", { couponCode: "SAVE10" });
};
return (
<View>
<Text>{title}</Text>
<Button title="Go to Cart" onPress={goToCart} />
</View>
);
}
Khi dùng useNavigation hook bên ngoài screen component:
import { useNavigation } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
function ProductCard({ product }: { product: Product }) {
const navigation = useNavigation<NavigationProp>();
return (
<TouchableOpacity
onPress={() =>
navigation.navigate("ProductDetail", {
productId: product.id,
title: product.title,
})
}
>
<Text>{product.title}</Text>
</TouchableOpacity>
);
}
useNavigation, khai báo global type.
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
Sau khai báo này, useNavigation() sẽ tự có type-safe mà không cần generic.
Lưu navigation state qua app restart (hữu ích cho development hoặc user experience):
import AsyncStorage from "@react-native-async-storage/async-storage";
const PERSISTENCE_KEY = "NAVIGATION_STATE_V1";
function App() {
const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState();
React.useEffect(() => {
const restoreState = async () => {
try {
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
const state = savedStateString
? JSON.parse(savedStateString)
: undefined;
setInitialState(state);
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
if (!isReady) return null;
return (
<NavigationContainer
initialState={initialState}
onStateChange={(state) =>
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))
}
>
<RootStack />
</NavigationContainer>
);
}
Khi cần navigate từ nơi không có access đến navigation prop (service, saga, interceptor), dùng navigationRef:
import { createNavigationContainerRef } from "@react-navigation/native";
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
export function navigateOutside(name: string, params?: object) {
if (navigationRef.isReady()) {
navigationRef.navigate(name as any, params as any);
}
}
Gắn ref vào NavigationContainer:
<NavigationContainer ref={navigationRef}>
<RootStack />
</NavigationContainer>
Sử dụng trong axios interceptor:
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
navigateOutside("Login");
}
return Promise.reject(error);
}
);
myapp://product/123 — đơn giản, chỉ hoạt động khi app đã càihttps://myapp.com/product/123 — link thật, fallback ra web nếu chưa cài appCấu hình linking trong React Navigation:
const linking = {
prefixes: ["myapp://", "https://myapp.com"],
config: {
screens: {
Main: {
screens: {
Home: {
screens: {
HomeMain: "home",
ProductDetail: "product/:productId",
},
},
Profile: "profile",
},
},
Cart: "cart",
NotFound: "*",
},
},
};
function App() {
return (
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
<RootStack />
</NavigationContainer>
);
}
Với config trên, URL myapp://product/123 sẽ navigate đến ProductDetail screen với route.params.productId = "123".
Test deep linking trong development:
npx uri-scheme open "myapp://product/123" --ios
adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.myapp
Dùng beforeRemove event để cảnh báo user trước khi rời khỏi screen có unsaved changes:
function EditProfileScreen({ navigation }) {
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
React.useEffect(() => {
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
if (!hasUnsavedChanges) return;
e.preventDefault();
Alert.alert(
"Bỏ thay đổi?",
"Bạn có thay đổi chưa lưu. Bạn chắc chắn muốn rời đi?",
[
{ text: "Ở lại", style: "cancel" },
{
text: "Rời đi",
style: "destructive",
onPress: () => navigation.dispatch(e.data.action),
},
]
);
});
return unsubscribe;
}, [navigation, hasUnsavedChanges]);
return <View>...</View>;
}
Pattern chuẩn: dùng conditional rendering để switch giữa Auth stack và Main stack dựa trên authentication state. Không dùng navigate/reset để chuyển — React Navigation sẽ tự handle transition animation.
function RootNavigator() {
const { isSignedIn, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />;
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isSignedIn ? (
<Stack.Screen name="Main" component={MainTabs} />
) : (
<Stack.Screen
name="Auth"
component={AuthStack}
options={{ animationTypeForReplace: "pop" }}
/>
)}
</Stack.Navigator>
);
}
react-native-screens sử dụng native view (UIViewController trên iOS, Fragment trên Android) thay vì giữ tất cả screen trong cùng một React Native view hierarchy. Screen không hiển thị sẽ bị detach khỏi view hierarchy → tiết kiệm memory.
import { enableScreens } from "react-native-screens";
enableScreens();
Từ React Navigation v6+, react-native-screens được enable tự động khi dùng @react-navigation/native-stack.
Mặc định, tất cả tab screen render ngay khi Tab Navigator mount. Dùng lazy để chỉ render khi user navigate đến tab đó:
<Tab.Navigator screenOptions={{ lazy: true }}>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Search" component={SearchStack} />
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
<Stack.Navigator screenOptions={{ detachPreviousScreen: true }}>
...
</Stack.Navigator>
Khi detachPreviousScreen: true, screen phía dưới sẽ bị unmount khi không nhìn thấy, giảm memory usage cho stack sâu.
Khi design yêu cầu tab bar khác mặc định (custom icon, animation, floating tab bar):
function CustomTabBar({ state, descriptors, navigation }) {
return (
<View style={{
flexDirection: "row",
backgroundColor: "#1a1d27",
paddingBottom: 20,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: "#2e3347",
}}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = options.tabBarLabel ?? options.title ?? route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
return (
<TouchableOpacity
key={route.key}
onPress={onPress}
style={{ flex: 1, alignItems: "center" }}
>
<Icon
name={getIconName(route.name)}
size={24}
color={isFocused ? "#6c63ff" : "#8b8fa8"}
/>
<Text style={{
color: isFocused ? "#6c63ff" : "#8b8fa8",
fontSize: 11,
marginTop: 4,
}}>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
}
<Tab.Navigator tabBar={(props) => <CustomTabBar {...props} />}>
...
</Tab.Navigator>
tabPress event thông qua navigation.emit() để các listener (như beforeRemove) hoạt động đúng. Không chỉ đơn giản gọi navigation.navigate().
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| Back về Login sau khi login | Navigation state sau auth, action đang dùng navigate hay reset | Dùng reset/conditional auth tree, clear stack cũ |
| Deep link mở sai screen | Linking config, nested path, app cold start, auth ready chưa | Chuẩn hóa URL, pending link sau login, test killed state |
| Screen cùng route bị reuse sai params | Dùng navigate hay push, route key, params thay đổi | Dùng push cho instance mới hoặc key riêng |
| Notification không mở đúng detail | Payload, navigationRef ready, auth state, linking fallback | Map notification thành deep link và queue khi app chưa ready |
Khung trả lời phỏng vấn: Navigation là state của màn hình. Khi trả lời, luôn nói về stack/tab state, action dùng để mutate state, edge cases như auth reset, hardware back, deep link cold start và notification open.
navigate() kiểm tra xem screen đã tồn tại trong stack chưa. Nếu có → quay về screen đó (pop các screen phía trên). Nếu chưa → push screen mới lên stack.
push() luôn tạo instance mới trên stack, bất kể screen đó đã tồn tại hay chưa.
Ví dụ cần push: Trong app chat, user đang xem chat của A, tap vào profile link đến chat của B. Nếu dùng navigate("Chat", { userId: B }), stack sẽ navigate về Chat screen cũ (của A) và update params. Dùng push("Chat", { userId: B }) sẽ tạo Chat screen mới cho B, giữ nguyên A phía dưới. User có thể back lần lượt: B → A → danh sách.
Tương tự với e-commerce: Product A → Related Product B → Related Product C — mỗi product cần instance riêng.
Cách chuẩn: Dùng conditional rendering trong navigator. Dựa trên auth state (từ Context/Redux), render Auth stack hoặc Main stack.
Tại sao không dùng navigate/reset:
/product/123 sẽ crash vì Main stack chưa mount. Với conditional rendering, NavigationContainer sẽ tự giữ pending link và navigate sau khi loginanimationTypeForReplace để transition mượt giữa Auth ↔ MainCơ chế: NavigationContainer nhận linking prop chứa prefixes và config. Khi app nhận URL (từ intent/scheme), React Navigation parse URL, match với config, và navigate đến screen tương ứng.
| Loại | Format | Ưu điểm | Nhược điểm |
|---|---|---|---|
| URL Scheme | myapp://path |
Setup đơn giản, không cần server | Không fallback web, có thể conflict với app khác, không verified |
| Universal Links (iOS) | https://domain.com/path |
Verified bởi Apple (AASA file), fallback web, an toàn | Cần domain + SSL + apple-app-site-association file trên server |
| App Links (Android) | https://domain.com/path |
Verified bởi Google (assetlinks.json), auto-open không cần chooser | Cần domain + SSL + assetlinks.json trên server |
Best practice: Production nên dùng Universal Links + App Links (cùng domain), đồng thời giữ URL Scheme cho development và testing.
Navigation state là một serializable plain JS object với cấu trúc: { routes: [...], index: number, type: string }. Mỗi route có { key, name, params, state? } — trong đó state lồng thêm cho nested navigator.
Lý do dùng plain object:
JSON.stringify/parse để persist state qua AsyncStorage, gửi qua deep link, hoặc log cho debuggingNative Stack (@react-navigation/native-stack):
JS Stack (@react-navigation/stack):
Rule of thumb: Bắt đầu với Native Stack. Chỉ chuyển sang JS Stack cho screen cụ thể khi cần animation mà Native Stack không hỗ trợ.
Dùng createNavigationContainerRef() để tạo ref, gắn vào NavigationContainer, rồi export hàm navigate sử dụng ref đó.
Lưu ý quan trọng:
navigationRef.isReady() trước khi gọi navigate, vì ref chưa sẵn sàng ngay khi app startonReady callback của NavigationContainer để queue lại actiondispatch(), reset(), getCurrentRoute() — đầy đủ navigation APIUse case phổ biến: 401 interceptor redirect về Login, push notification handler mở specific screen, analytics tracking current screen.
Vấn đề thường gặp:
headerShown: false cho parent hoặc childNavigate đến nested screen:
navigation.navigate("Main", {
screen: "Home",
params: {
screen: "ProductDetail",
params: { productId: "123" },
},
});
Hoặc dùng deep linking config — URL myapp://product/123 sẽ tự resolve đến đúng nested screen.
Cách 1 — beforeRemove event: Intercept back action, hiện confirmation dialog. Phù hợp khi có unsaved changes (form, editor).
Cách 2 — gestureEnabled: false: Tắt swipe back gesture (iOS). Kết hợp với headerBackVisible: false để ẩn nút back.
Cách 3 — replace/reset: Thay thế screen hiện tại hoặc reset toàn bộ stack. User không có screen nào phía trước để quay lại.
Ví dụ thực tế:
reset để đưa về OrderDetail, không cho quay lại Payment screen (tránh double charge)replace cho mỗi step, user không back được về step trướcbeforeRemove để hỏi "Bạn có muốn lưu thay đổi?" khi user backXây dựng hệ thống navigation hoàn chỉnh cho ứng dụng e-commerce.
myshop://product/:id, myshop://category/:slug, myshop://order/:orderIdmyshop://product/123 mở đúng ProductDetail khi đã login, redirect về Login khi chưaSau login, reset stack để Android back không quay về Login. Sau logout, clear sensitive state và reset về Auth stack.
Mở app từ myshop://product/123 khi app chưa chạy. Nếu chưa login, lưu pending link, login xong navigate tới ProductDetail.
Dùng beforeRemove cho EditProfile. Back hardware Android, gesture iOS và header back đều phải hiện confirm.
navigate thay vì push cho screen cùng route nhưng params khác.navigate("Home"), Android back quay lại Login.Case: Push notification mở OrderDetail đúng khi app đang foreground, nhưng khi app bị kill thì chỉ mở Home.
Cách xử lý: Chuẩn hóa notification payload thành deep link, cấu hình linking cho cold start, và lưu pending navigation cho trường hợp auth chưa sẵn sàng.
Navigation tốt là quản lý state stack, không chỉ chuyển màn hình. Luôn test: login/logout, hardware back, deep link cold start, notification open, và unsaved changes.
push thay vì navigate?