Module 05 — Navigation

PHẢI CÓ ? 6-8 giờ 📋 Prerequisites: Module 01, 02, 03, 04

🎯 MỤC TIÊU HỌC TẬP

📖 HƯỚNG DẪN HỌC

1

Đọc tài liệu chính thức

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.

2

Setup project với nested navigators

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.

3

Implement type-safe navigation

Đọ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.

4

Implement Deep Linking

Đọ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.

🧭 Vòng lặp học module này

📚 LÝ THUYẾT CHI TIẾT

1. React Navigation Architecture

NavigationContainer là root component bao bọc toàn bộ navigation tree. Nó quản lý navigation state, linking, và cung cấp context cho tất cả navigator/screen bên trong.

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

2. Navigator Types — Khi nào dùng cái nào

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
⚠️ Native Stack vs JS Stack: Luôn ưu tiên Native Stack (@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.

3. Nested Navigation

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).

Stack chứa Modal

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.

4. Screen Options

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

Custom Header

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

5. Navigation Actions — Navigate vs Push vs Replace vs Reset

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 vs push: Giả sử stack là [A, B, C] và bạn đang ở C. Gọi 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ớ.

6. Passing Params & Type-Safe Navigation

Để 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>;
}

Type-Safe với TypeScript

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>
  );
}
🔑 Global type declaration: Để không phải truyền type mỗi lần dùng 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.

7. Navigation State Persistence

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>
  );
}
⚠️ Chú ý: State persistence nên chỉ enable trong development hoặc có version check. Nếu navigation structure thay đổi giữa các bản update, state cũ có thể crash app. Luôn wrap trong try-catch.

8. Navigate từ bên ngoài Component — navigationRef

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

9. Deep Linking

Deep Linking cho phép mở một screen cụ thể trong app từ URL bên ngoài (notification, email, browser). Có 3 cách:

Cấ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

10. Prevent Going Back — beforeRemove

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

11. Auth Flow Pattern

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>
  );
}
✅ Lợi ích pattern này:

12. Performance Optimization

react-native-screens

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.

Lazy Loading Screens

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>

Detach Inactive Screens (Stack)

<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.

13. Custom Tab Bar

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>
💡 Quan trọng: Khi tạo custom tab bar, phải emit 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().

❓ CÂU HỎI PHỎNG VẤN + ĐÁP ÁN

🛠️ Debug playbook: navigation bug

Triệu chứngKiểm traFix thường gặp
Back về Login sau khi loginNavigation state sau auth, action đang dùng navigate hay resetDùng reset/conditional auth tree, clear stack cũ
Deep link mở sai screenLinking config, nested path, app cold start, auth ready chưaChuẩn hóa URL, pending link sau login, test killed state
Screen cùng route bị reuse sai paramsDùng navigate hay push, route key, params thay đổiDùng push cho instance mới hoặc key riêng
Notification không mở đúng detailPayload, navigationRef ready, auth state, linking fallbackMap 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.

Q1: navigate() và push() khác nhau như thế nào? Cho ví dụ khi nào dùng push thay vì navigate.

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.

Q2: Mô tả cách triển khai Auth flow chuẩn trong React Navigation. Tại sao không nên dùng navigate/reset?

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:

  • Security: Với navigate/reset, Auth screen vẫn tồn tại trong memory (chỉ bị ẩn), có thể bị access bằng gesture hoặc hardware back button
  • Deep linking: Nếu user chưa login, deep link vào /product/123 sẽ crash vì Main stack chưa mount. Với conditional rendering, NavigationContainer sẽ tự giữ pending link và navigate sau khi login
  • State management: Khi logout, nếu dùng reset, phải manually clear toàn bộ navigation state. Với conditional, React tự unmount Main stack → clean up tất cả
  • Animation: React Navigation cung cấp animationTypeForReplace để transition mượt giữa Auth ↔ Main
Q3: Deep Linking hoạt động như thế nào trong React Navigation? Phân biệt URL Scheme, Universal Links, App Links.

Cơ 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ạiFormatƯu điểmNhượ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.

Q4: Navigation state trong React Navigation được tổ chức như thế nào? Tại sao nó là plain object?

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:

  • Serialization: Có thể JSON.stringify/parse để persist state qua AsyncStorage, gửi qua deep link, hoặc log cho debugging
  • Predictable: Có thể inspect, test, và reproduce bất kỳ navigation state nào
  • Time-travel debugging: Lưu lại chuỗi state changes, replay lại navigation history
  • State restoration: Khi app restart, restore lại đúng screen + params mà user đang xem
Q5: Khi nào dùng Native Stack vs JS Stack? Trade-off là gì?

Native Stack (@react-navigation/native-stack):

  • Dùng UINavigationController (iOS) và Fragment (Android) native
  • Performance tốt nhất: animation chạy trên native thread, không bị ảnh hưởng bởi JS thread busy
  • Hạn chế: ít customizable hơn, một số animation/transition không thể override

JS Stack (@react-navigation/stack):

  • Render hoàn toàn bằng React Native + Animated/Reanimated
  • Full control: custom transition animation, shared element transition, card style override
  • Hạn chế: animation chạy trên JS thread, có thể jank khi JS thread busy

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ợ.

Q6: Làm sao navigate từ bên ngoài React component (ví dụ: API interceptor, push notification handler)?

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:

  • Phải check navigationRef.isReady() trước khi gọi navigate, vì ref chưa sẵn sàng ngay khi app start
  • Nếu cần navigate ngay khi app start (từ notification), dùng onReady callback của NavigationContainer để queue lại action
  • Ref cũng hỗ trợ dispatch(), reset(), getCurrentRoute() — đầy đủ navigation API

Use case phổ biến: 401 interceptor redirect về Login, push notification handler mở specific screen, analytics tracking current screen.

Q7: Nested navigation có vấn đề gì cần lưu ý? Làm sao navigate đến screen trong nested navigator?

Vấn đề thường gặp:

  • Double header: Khi Stack chứa Tab hoặc Tab chứa Stack, cả hai đều render header → phải set headerShown: false cho parent hoặc child
  • Navigation action scope: Mỗi navigator chỉ handle action cho screen trực tiếp của nó. Navigate đến screen trong nested navigator cần specify đường dẫn đầy đủ
  • Params not forwarded: Params chỉ được gửi đến target screen, không tự forward xuống nested screen

Navigate đế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.

Q8: Làm sao ngăn user quay lại screen trước? Cho ví dụ thực tế.

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ế:

  • Payment Success: Sau khi thanh toán thành công, dùng reset để đưa về OrderDetail, không cho quay lại Payment screen (tránh double charge)
  • Onboarding: Dùng replace cho mỗi step, user không back được về step trước
  • Edit Profile: Dùng beforeRemove để hỏi "Bạn có muốn lưu thay đổi?" khi user back

🏋️ BÀI TẬP

Bài tập: E-Commerce Navigation System

Xây dựng hệ thống navigation hoàn chỉnh cho ứng dụng e-commerce.

Yêu cầu cấu trúc navigation:

Yêu cầu kỹ thuật:

  1. Type-safe navigation với TypeScript ParamList cho tất cả screen
  2. Auth flow pattern: conditional Auth/Main stack dựa trên auth state
  3. Deep linking config: myshop://product/:id, myshop://category/:slug, myshop://order/:orderId
  4. Custom tab bar với badge count cho Cart tab
  5. beforeRemove trên EditProfile (unsaved changes warning)
  6. reset navigation sau PaymentSuccess (không cho back về Checkout)
  7. navigationRef: navigate đến OrderDetail từ push notification handler
  8. Lazy loading cho tất cả tab

Acceptance Criteria:

🧩 MINI EXERCISE THỰC TẾ REACT NATIVE

Exercise 1: Auth reset stack

Sau login, reset stack để Android back không quay về Login. Sau logout, clear sensitive state và reset về Auth stack.

Exercise 2: Deep link cold start

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.

Exercise 3: Unsaved changes guard

Dùng beforeRemove cho EditProfile. Back hardware Android, gesture iOS và header back đều phải hiện confirm.

⚠️ LỖI THƯỜNG GẶP

🏢 CASE ĐI LÀM THẬT

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.

🧠 GHI NHỚ NHANH

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.

❔ CÂU HỎI TỰ KIỂM TRA

1. Khi nào dùng push thay vì navigate?
2. Login/logout nên dùng action gì để tránh back sai?
3. Deep link cần xử lý khác nhau thế nào giữa warm start và cold start?
4. Params nào không nên đưa vào navigation state?

✅ CHECKLIST TỰ ĐÁNH GIÁ

📎 TÀI NGUYÊN

📘 React Navigation — Getting Started (Official Docs)
📘 React Navigation — Type Checking with TypeScript
📘 React Navigation — Deep Linking
📘 React Navigation — Authentication Flow
📘 React Navigation — Nesting Navigators
📘 React Navigation — Screen Options
📘 React Navigation — Custom Navigators
📹 React Navigation 6 Full Tutorial — Academind (YouTube)
🛠 React Navigation GitHub Repository
📘 react-native-screens — GitHub