react-navigation筆記
安裝依賴
npm install react-native-gesture-handler@1.7.0 react-native-reanimated@1.12.0 react-native-screens@2.10.1 react-native-safe-area-context@3.1.7 @react-native-community/masked-view@0.1.10 @react-navigation/compat@5.2.5
@react-navigation/native@5.7.3 @react-navigation/stack@5.9.0
在根文件頂部引入,否則應用會崩潰
import 'react-native-gesture-handler';
將整個應用包裝在 NavigationContainer
中
import 'react-native-gesture-handler';
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
);
}
安裝 @react-navigation/stack
npm install @react-navigation/stack
createStackNavigator
是一個返回一個對象(包含2個屬性 Screen
和 Navigator
)的函數。 Navigator
應該包含子元素 Screen
,來定義路由配置。用 NavigationContainer
包裹 Navigator
// In App.js in a new project
import * as React from 'react';
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
function DetailsScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
</View>
);
}
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{ gestureEnabled: true }}>
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Overview' }}/>
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
配置
Screen
的必需配置屬性是 name
和 component
。在 Navigator
上配置 initialRouteName
。
component
屬性接受組件,不接受一個render函數。不要傳一個內聯函數例如 component={() => <HomeScreen />}
,否則當父組件re-render時,你的組件將卸載和重載失去的所有state。
每個路由的配置可以放在 options
里面。相同的可以配在 Navigator
的 screenOptions
里面。
傳遞額外的props:
- (推薦)使用React Context,用context provider包裹navigator去傳數據到screens。
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
- 用render callback而不是定義一個component prop。默認情況下react-navigation底層做了優化防止重復渲染,但是使用render callback則會移除這些優化。如果用render callback,需要確保Screen組件使用了
React.memo
或React.PureComponent
<Stack.Screen name="Home">
{props => <HomeScreen {...props} extraData={someData} />}
</Stack.Screen>
路由跳轉
import * as React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}
// ... other code from the previous section
navigation
屬性會傳遞給每個在stack navigator中定義的screen component。navigation.navigate('Detail')
調用navigate方法傳入路由name,可以跳轉到對應頁面。navigation.push('Detail')
可以做到在Detail頁面打開另一個Detail頁面。navigation.goBack()
路由回退。-
回退多個頁面的方式:
navigate('Home')
回退到Home頁。navigation.popToTop()
會對到首頁。
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again" onPress={() => navigation.push('Details')} />
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
<Button title="Go back to first screen in stack" onPress={() => navigation.popToTop()}
/>
</View>
);
}
路由傳參
步驟:
-
navigation.navigate('RouteName', { /* params go here */ })
,推薦使用JSON格式的參數。 - 在screen組件中讀取傳入的參數
route.params
。
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => {
/* 1. Navigate to the Details route with params */
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
}}
/>
</View>
);
}
function DetailsScreen({ route, navigation }) {
/* 2. Get the param */
const { itemId } = route.params;
const { otherParam } = route.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details', { itemId: Math.floor(Math.random() * 100) }}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}
更新路由參數: navigation.setParams
。
傳參回到上一個頁面: navigation.navigate('',params)
傳參到嵌套的路由器:
navigation.navigate('Account', {
screen: 'Settings',
params: { user: 'jane' },
});
配置標題欄
設置標題:(在 Screen
上設置 options
)
options={{ title: 'My home' }}
-
options={({ navigation, route }) => ({ title: route.params.name })}
,options
函數的參數包括navigation
和route
,我們只需用到route
- 使用
navigation.setOptions({ title:'Updated!' })
更新options
修改header樣式:(在 Screen
上設置 options
)
-
headerStyle
:參考View的樣式寫法 headerTintColor
-
headerTitleStyle
:參考Text的樣式寫法 - 注意:iOS上,status bar的文字和圖標是黑色的,詳見適配方案
復用常規options:
- 在
Stack.Navigator
上配置屬性screenOptions
自定義組件替換標題: headerTitle
(默認是展示text的Text組件)
其他options:詳見文檔
function LogoTitle() {
return (
<Image
style={{ width: 50, height: 50 }}
source={require('@expo/snack-static/react-native-logo.png')}
/>
);
}
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerTitle: props => <LogoTitle {...props} /> }}
/>
</Stack.Navigator>
);
}
header 按鈕
添加header按鈕
- headerRight
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
// 在options里 this 不指向HomeScreen實例,不能調用setState或其他實例方法
headerTitle: props => <LogoTitle {...props} />,
headerRight: () => (
<Button
onPress={() => alert('This is a button!')}
title="Info"
color="#fff"
/>
),
}}
/>
</Stack.Navigator>
);
}
header與它的screen組件交互
- 定義按鈕時,在screen組件內使用
navigation.setOptions
而不是options
屬性,這樣可以獲取screen的props、state、context等。
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={({ navigation, route }) => ({
headerTitle: props => <LogoTitle {...props} />,
})}
/>
</Stack.Navigator>
);
}
function HomeScreen({ navigation }) {
const [count, setCount] = React.useState(0);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<Button onPress={() => setCount(c => c + 1)} title="Update count" />
),
});
}, [navigation]);
return <Text>Count: {count}</Text>;
}
配置回退按鈕
-
createStackNavigator
提供平臺定制的默認回退按鈕。 headerBackTitle
-
headerTruncatedBackTitle
:文檔 -
headerBackImage
:文檔
覆蓋回退按鈕
headerRight
headerLeft
- 保留button只覆蓋
onPress
方法,可以從@react-navigation/stack
導出HeaderBackButton
組件分配給headerLeft
選項
嵌套導航器(todo)
在一個導航器的Screen內渲染另一個導航器。
-
Stack.Navigator
-
Home
(Tab. Navigator
)-
Feed
(Screen
) -
Messages
(Screen
)
-
-
Profile
(Screen
) -
Settings
(Screen
)
-
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={Feed} />
<Tab.Screen name="Messages" component={Messages} />
</Tab.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
</NavigationContainer>
);
}
注意:
- 每個嵌套導航器保持自己的導航歷史
- 每個嵌套導航器有自己的
options
- 每個嵌套導航器有自己的參數
- 如果需要從父Screen傳參到子Screen,可以用React Context透傳參數給children
- 導航actions(比如
goBack
、navigate
)會被當前導航器處理,如果處理不了則會冒泡(被父導航器)處理 - 導航器的特定方法在嵌套導航器中也可用:
- 如果需要從父導航器dispatch actions到嵌套的子導航器,可以使用
navigation.dispatch
- 如果需要從父導航器dispatch actions到嵌套的子導航器,可以使用
// 從父導航器dispatch actions到嵌套的子導航器
navigation.dispatch(DrawerActions.toggleDrawer());
{/** Drawer的openDrawer方法可以被Stack調用(navigation.openDrawer) */}
<Drawer>
<Stack />
{/** ... */}
</Drawer>
{/** Drawer的openDrawer方法不可被Stack下的其他Screen調用 */}
<Stack>
<Drawer />
{/** ... */}
</Stack>
{/** Tab可以調用Stack的方法push和replace(navigation.push、navigation.replace) */}
<Stack>
<Tab/>
{/** ... */}
</Stack>
嵌套導航器不接收父導航器的事件,如需接受父導航器的事件,需要使用 navigation.dangerouslyGetParent()
顯式監聽
const unsubscribe = navigation
.dangerouslyGetParent()
.addListener('tabPress', (e) => {
// Do something
});
父導航器的UI渲染在子導航器的最頂端:
- Drawer的每一個頁面中嵌套Stack:抽屜出現在堆棧標題的上方
- Stack中嵌套Drawer:抽屜顯式在標題下方
- Stack的首頁中嵌套Tab:新頁面會覆蓋掉tab bar
- Tab的每一個頁面中嵌套Stack:tab bar一直顯示,再按一下tab會回到stack的頂部
在嵌套導航器中跳轉頁面
-
Drawer.Navigator
-
Drawer.Screen
:Home
-
Drawer.Screen
:Root
-
Stack.Screen
:Profile
-
Stack.Screen
:Settings
-
-
navigation.navigate('Root');
navigation.navigate('Root', { screen: 'Settings' });
在以前的版本中,所有配置都是靜態的,但是現在通過動態配置,直到包含Screen的導航器呈現之前,react-navigation才知道哪些Screen可用以及在哪里可用。通常,Screen在導航進入之前不會渲染任何東西,因此尚未渲染的導航器配置不可用。這使得必須制定要導航到的層次結構。這也是為什么應該盡可能減少嵌套導航器以使代碼更簡單的原因。
將參數傳遞給嵌套導航器中Screen
// 指定params傳遞參數
navigation.navigate('Root', {
screen: 'Settings',
params: {
user: 'jane'
},
});
// 深層嵌套
navigation.navigate('Root', {
screen: 'Settings',
params: {
screen: 'Sound',
params: {
screen: 'Media',
},
},
});
渲染定義在導航器中的初始路由
默認情況下,在嵌套導航器中導航Screen時,指定的screen會被用作初始screen,并且導航器上的initialRoute prop會被忽略。
如果需渲染特定的初始路由頁面,可以設置 initial: false
。
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
嵌套多個stack 導航器
當嵌套多個Stack導航器時,react-navigation將自動隱藏子stack導航器的標題,以避免重復的標題。但是根據場景不同,顯示子stack導航器的標題,而不是隱藏父stack導航器的標題,可能更有用。可以設置 headerShown: false
解決該問題。完整示例 極少的情況,你需要同事顯示父stack導航器和子stack導航器的標題,可以在子導航器上設置 headerShown: true
function Home() {
return (
<NestedStack.Navigator>
<NestedStack.Screen name="Profile" component={Profile} />
<NestedStack.Screen name="Settings" component={Settings} />
</NestedStack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<RootStack.Navigator>
<RootStack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
<RootStack.Screen name="EditPost" component={EditPost} />
</RootStack.Navigator>
</NavigationContainer>
);
}
嵌套的最佳做法:建議將嵌套導航器減少到最小。嘗試通過盡可能少的嵌套來實現所需的行為。缺點如下
- 會導致深度嵌套的視圖層級結構,從而可能導致低端設備出現內存和性能問題
- 嵌套相同類型的導航器可能會導致混淆的用戶體驗
- 如果嵌套過多,導航到嵌套頁面,配置深層鏈接等時將很難編寫代碼
導航生命周期
堆棧導航器中包含A和B兩個screen。當導航到A頁面時,A的 componentDidMount
被調用,當跳轉到B時,B的 componentDidMount
被調用,但是A在stack中保持mounted,A的 componentWillUnmount
沒有被調用。
當從B回退到A時,B的 componentWillUnmmount
被調用,但是A的 componentDidMount
沒有被調用,因為A一直是mounted狀態。
生命周期事件文檔
focus
blur
function Profile({ navigation }) {
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// Screen was focused
// Do something
});
return unsubscribe;
}, [navigation]);
return <ProfileContent />;
}
- 代替手動添加事件監聽,可以用
useFocusEffect
hook去執行副作用,類似react的useEffect
hook。 - 使用
useIsFocused
hook:返回boolean,表示屏幕是否聚焦
import { useFocusEffect } from '@react-navigation/native';
function Profile() {
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, [])
);
return <ProfileContent />;
}
打開全屏modal(需要嵌套導航器知識)
在Navigator上設置屬性 mode="modal"
,調用 navigation.navigate
打開modal
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ fontSize: 30 }}>This is the home screen!</Text>
<Button
onPress={() => navigation.navigate('MyModal')}
title="Open Modal"
/>
</View>
);
}
function DetailsScreen() {
return (
<View>
<Text>Details</Text>
</View>
);
}
function ModalScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ fontSize: 30 }}>This is a modal!</Text>
<Button onPress={() => navigation.goBack()} title="Dismiss" />
</View>
);
}
const MainStack = createStackNavigator();
const RootStack = createStackNavigator();
function MainStackScreen() {
return (
<MainStack.Navigator>
<MainStack.Screen name="Home" component={HomeScreen} />
<MainStack.Screen name="Details" component={DetailsScreen} />
</MainStack.Navigator>
);
}
function RootStackScreen() {
return (
<RootStack.Navigator mode="modal">
<RootStack.Screen
name="Main"
component={MainStackScreen}
options={{ headerShown: false }}
/>
<RootStack.Screen name="MyModal" component={ModalScreen} />
</RootStack.Navigator>
);
}
注意:
-
mode
可以設置為card
(默認)和modal
(iOS:從底部劃出,從頂部向下滑動以關閉;安卓:無效) - 當調用
navigate
時,除了路由外,無需指定其他任何內容。(react-navigation嘗試在最近的導航器上查找路由,然后在該位置執行操作)
Tab Navigator(todo)
術語表
header: 屏幕頂端的矩形,包含回退按鈕和標題
Navigator
:包含子元素 Screen
。NavigationContainer是一個管理導航樹和包含導航狀態的組件。這個組件必須包著所有的導航結構。通常我們在app的頂部(通常在App.js里)渲染這個組件。
function App() {
return (
<NavigationContainer>
<Stack.Navigator> // <---- This is a Navigator
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Screen 組件:定義路由配置的組件
被配置在路由配置里的Screen組件(被 navigation.navigate
喚起的Screen)被提供了 navigation
props,
const Stack = createStackNavigator();
const StackNavigator = (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen} // <----
/>
<Stack.Screen
name="Details"
component={DetailsScreen} // <----
/>
</Stack.Navigator>
);
Navigation prop navigation prop文檔
dispatch
navigate
goBack
- ...
Route proproute prop文檔
params
key
name
Navigation state:結構如下,index指向當前的路由(B)
{
key: 'StackRouterRoot',
index: 1,
routes: [
{ key: 'A', name: 'Home' },
{ key: 'B', name: 'Profile' },
]
}
Route
{
key: 'B',
name: 'Profile',
params: { id: '123' }
}
兼容性層:降低升級的代碼改動成本
- 安裝
@react-navigation/compat
:
npm install @react-navigation/native @react-navigation/compat @react-navigation/stack
- 使用
createCompatNavigatorFactory
包裹
-import { createStackNavigator } from 'react-navigation-stack';
+import { createStackNavigator } from '@react-navigation/stack';
+import { createCompatNavigatorFactory } from '@react-navigation/compat';
-const RootStack = createStackNavigator(
+const RootStack = createCompatNavigatorFactory(createStackNavigator)(
{
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
},
{
initialRouteName: 'Profile',
}
);
-
NavigationActions
改為從@react-navigation/compat
中導出
-import { NavigationActions } from 'react-navigation';
+import { NavigationActions } from '@react-navigation/compat';
-
@react-navigation/compat
導出的api包括:- Actions:
NavigationActions
StackActions
DrawerActions
SwitchActions
- HOCs
withNavigation
withNavigationFocus
- Navigators
createSwitchNavigator
- Compatibility helpers
-
createCompatNavigatorFactory
- 傳入一個使用v5的api的導航器,返回一個使用v4 api的createXNavigator
-
createCompatNavigationProp
- 傳入一個v5 的navigation
對象 和route對象,返回一個v4的navigation
對象
-
- Actions:
-
兼容層處理了:
- 使用v4的靜態配置api,俄日不是基于api的組件
- 改變了
navigation
對象上的方法名稱去適配v4 - 添加了對
screenProps
的支持(v5上被移除) - 導出了跟v4同名的action creators,例如
NavigationActions
、StackActions
、SwitchActions
。
-
兼容層沒處理:v5動態api導致v4靜態api不再具有某些功能
- 不再包含navigator的props或者options。意味著你要傳遞給navigator的options可能因為重大改變而不同。
- 通過在路由配置中定義path來定義舊式深層鏈接是不支持的
- 導航到導航器的工作原理不同,我們無法導航到尚未渲染的導航器中的屏幕,并且參數不會合并到所有子屏幕。
- 某些采取一系列操作的方法例如
reset
將不再支持。 - 不導出
createAppContainer
,需要對容器使用v5的apiNavigationContainer
。 - 如果使用的高階api例如Redux集成、自定義路由器、actions,這些不再支持,并且你需要移除Redux集成。
-
為什么用:
- 允許使用新api寫代碼,同時使用舊api與代碼集成
- 建立在支持TypeScript的v5之上,舊代碼也可以利用類型檢查功能,對重構有幫助
- 可以在舊組件中訪問新api,例如
navigation.setOptions
或者一些新的hook例如useFocusEffect
。
在沒用navigation props時navigate
有時你需要從一個沒有
navigation
prop的地方觸發navigation action,比如redux middleware,在這種時候,你可以從navigation容器中dispatch navigation actions。如果你想要找一種方式,從一個不需要傳
navigation
prop下去的組件里面navigate,可以用useNavigation
。當你可以拿到navigation prop或者useNavigation
時不要用這個方法,因為它的行為會有所不同,而且許多特定于屏幕的helper方法將不可用。你可以通過
ref
訪問根導航器對象,將其傳遞給RootNavigation
,我們稍后將使用該RootNavigation
進行導航。
// App.js
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}