這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
又是一些跟路由相關的內容,想到什麼就分享什麼吧。
TS 型別檢查
假設我的應用有兩個 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
表示該頁面沒有 propsHome: { 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 的型別,包含了兩個重要的屬性:route
和 navigation
。
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) => {
// ...
}
假設我今天想從 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) => {
//...
}
所以我們可以新增一個 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),因為路由並沒有改變。
我的解決辦法是去監聽用戶是不是按了返回鍵,如果是的話就中斷回到上一頁的操作,然後將 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} />
這個方法還可以用在返回時提醒用戶保存當前表單內容(如果有修改的話)。