React Native 奇幻之旅(14)-React Navigation TS 型別檢查

React Native logo

這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408

又是一些跟路由相關的內容,想到什麼就分享什麼吧。

TS 型別檢查

Navigator

假設我的應用有兩個 Navigation Stack:

  • LoginStack
    • Login
    • Register
    • ForgotPassword
  • AuthStack
    • Home
    • List
    • Setting
// App.tsx
<NavigationContainer>
    <Stack.Navigator initialRouteName="LoginStack">
      <Stack.Screen
        name="LoginStack"
        component={LoginStackNavigator}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="AuthStack"
        component={LoginStackNavigator}
        options={{ headerShown: false }}
      />
    </Stack.Navigator>
</NavigationContainer>

首先需要定義路由參數結構:

  • ParamList 的 key 為路由名稱,value 為路由參數型別
  • undefined 表示該頁面沒有 props
  • Home: { userId: number } 代表 Home 頁面有 userId 的 prop,並且是 number
export type LoginStackParamList = {
  Login: undefined
  Register: undefined
  ForgotPassword: undefined
}

export type AuthStackParamList = {
  Home: {
      userId: number
  }
  List: undefined
  Setting: undefined
}

定義 Navigator 的參數型別:

  • LoginStackParamList 所定義的各頁面參數型別,Login, Register 和 ForgotPassword 三個頁面參數皆為 undefined,也就是沒有參數。
// LoginStackNavigator.tsx
import { createStackNavigator } from '@react-navigation/stack'
import { LoginStackParamList } from '@types'

const Stack = createStackNavigator<LoginStackParamList>()

完整寫法:

// LoginStackNavigator.tsx
import React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { LoginScreen, RegisterScreen, ForgotPasswordScreen } from '@screens'
import { LoginStackParamList } from '@types'

const Stack = createStackNavigator<LoginStackParamList>()

export const LoginStackNavigator = () => {
  return (
    <Stack.Navigator initialRouteName="Login">
      <Stack.Screen
        name="Login"
        component={LoginScreen}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="Register"
        component={RegisterScreen}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="ForgotPassword"
        component={ForgotPasswordScreen}
        options={{ headerShown: false }}
      />
    </Stack.Navigator>
  )
}

AuthStackNavigator 也是一樣的,就不重複說明了。

// AuthStackNavigator.tsx
import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { HomeScreen, ListScreen, SettingScreen } from '@screens'
import { AuthStackParamList } from '@types'

const Tab = createBottomTabNavigator<AuthStackParamList>()

export const AuthStackNavigator = (): JSX.Element => {
  return (
    <Tab.Navigator initialRouteName="Home">
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        initialParams={{ userId: user.id }}
      />
      <Tab.Screen
        name="List"
        component={ListScreen}
      />
      <Tab.Screen
        name="Setting"
        component={SettingScreen}
      />
    </Tab.Navigator>
  )
}

注意:定義 navigation 參數結構的型別必須使用 type(例如 type RootStackParamList = { ... }),不能使用 interface(例如 interface RootStackParamList { ... })也不能使用 extends ,如:interface RootStackParamList extends ParamListBase { ... })。

Screen

NativeStackScreenProps 是用來表示 Stack Navigator 中 Screen 組件的 props 的型別,包含了兩個重要的屬性:routenavigation

  • route:當前 Screen 的路由(route)相關信息,例如 route name, params…等。
  • navigation:提供導航功能的相關方法,例如導航到其他畫面(navigate, replace, reset…等)、返回上一頁(goBack)等。
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { LoginStackParamList } from '@types'

type LoginScreenProps = NativeStackScreenProps<LoginStackParamList>

export const LoginScreen = ({ route, navigation }: LoginScreenProps) => {
    // ...   
}

Nesting navigators

假設我今天想從 LoginStack 中的 Login 頁面跳轉到 AuthStack 中的 Setting 頁面,它會提示只能跳轉到 Login, Register 和 ForgotPassword 三個頁面,這是因為 LoginStackParamList 中只有定義這三個頁面的映射:

// @types/navigation.ts
export type LoginStackParamList = {
  Login: undefined
  Register: undefined
  ForgotPassword: undefined
}

// LoginScreen.tsx
type LoginScreenProps = NativeStackScreenProps<LoginStackParamList>

export const LoginScreen = ({ navigation }: LoginScreenProps) => {
    //...
}
2qZ9GkJ

所以我們可以新增一個 RootStackParamList 用於定義應用全部 Stack 的參數型別,透過嵌套的方式可以在任意頁面導航到隨便一個 Stack 底下的 Screen:

// @types/navigation.ts
import type { NavigatorScreenParams } from '@react-navigation/native'

export type RootStackParamList = {
  LoginStack: NavigatorScreenParams<LoginStackParamList>
  AuthStack: NavigatorScreenParams<AuthStackParamList>
}

export type LoginStackParamList = {
  Login: undefined
  Register: undefined
  ForgotPassword: undefined
}

export type AuthStackParamList = {
  Home: {
      userId: number
  }
  List: undefined
  Setting: undefined
}

// LoginScreen.tsx
type LoginScreenProps = NativeStackScreenProps<RootStackParamList>

export const LoginScreen = ({ navigation }: LoginScreenProps) => {
    //...
}

這樣寫可以從 LoginStack 的 Login 頁面跳轉至 AuthStack 底下的 Setting 頁面:

navigation.navigate('AuthStack', { screen: 'Setting' })

自定義回到上一頁操作

有時候會遇到一種情況是當前表單頁面(FormPage) 的 header 有一個返回鍵,而表單中可以拍照,拍照是藉由 state 的改變去顯示相機畫面。那這樣就會遇到一個問題,如果在 state 為 true (相機開啟)時按下 header 的返回鍵,預期應該是要回到表單頁面(FormPage)但其實會回到表單的上一頁(ListPage),因為路由並沒有改變。

ref6ueg

我的解決辦法是去監聽用戶是不是按了返回鍵,如果是的話就中斷回到上一頁的操作,然後將 state 設為 false:

  • beforeRemove: 當用戶離開當前頁面時會觸發
useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
        // Prevent default behavior of leaving the screen
        e.preventDefault()
        // Your go back function
        goBack()
    })
    return unsubscribe
}, [navigation])

官方文檔中還有提到為了使這個方法有效需要將這個頁面的 gestureEnabled 設為 false,並且使用自定義的 back button 替換掉 native 的 back button headerLeft: (props) => <CustomBackButton {...props} />

這個方法還可以用在返回時提醒用戶保存當前表單內容(如果有修改的話)。

參考資料

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *