最简 Stack
// app/_layout.tsx import { Stack } from 'expo-router'; export default function RootLayout() { return <Stack />; }
就这一行。所有同级 .tsx 自动变成 Stack 的 Screen,title 默认取文件名。
配置单个 Screen
// app/product/[id].tsx import { Stack } from 'expo-router'; import { View, Text } from 'react-native'; export default function Product() { return ( <> <Stack.Screen options={{ title: '商品详情', headerStyle: { backgroundColor: '#000020' }, headerTintColor: '#fff', presentation: 'modal', // modal / card / transparentModal }} /> <View><Text>...</Text></View> </> ); }
Stack.Screen 放哪里
两个等效位置:屏幕组件里面(方便就近配置),或
两个等效位置:屏幕组件里面(方便就近配置),或
_layout.tsx 里(集中管理)。实际项目推荐「共性在 _layout,个性在屏幕内」。
screenOptions 全局默认
export default function RootLayout() { return ( <Stack screenOptions={{ headerStyle: { backgroundColor: '#fff' }, headerTitleStyle: { fontWeight: '600' }, headerBackTitle: '', // iOS 去掉返回文字 animation: 'slide_from_right', contentStyle: { backgroundColor: '#f5f5f5' }, }} /> ); }
常用 options
| option | 说明 |
|---|---|
| title | Header 标题 |
| headerShown | 是否显示 Header(默认 true) |
| headerLeft / headerRight | 自定义左/右按钮,支持函数返回组件 |
| headerTransparent | 沉浸式 Header(内容会到状态栏下) |
| headerLargeTitle | iOS 大标题(滚动收起) |
| presentation | card / modal / transparentModal / containedModal |
| animation | default / slide_from_right / slide_from_bottom / fade / none |
| gestureEnabled | 是否允许手势返回 |
| fullScreenGestureEnabled | iOS 全屏手势返回 |
自定义 Header 按钮
<Stack.Screen options={{ title: '编辑', headerRight: () => ( <Pressable onPress={handleSave}> <Text style={{ color: '#000020', fontWeight: '600' }}>保存</Text> </Pressable ), headerLeft: () => ( <Pressable onPress={() => router.back()}> <Text>取消</Text> </Pressable ), }} />
Modal 呈现
app/ ├── _layout.tsx ├── index.tsx └── login.tsx ← 这屏当作 modal 呈现
// app/_layout.tsx 集中配置 <Stack> <Stack.Screen name="index" options={{ title: '首页' }} /> <Stack.Screen name="login" options={{ presentation: 'modal', animation: 'slide_from_bottom', title: '登录', }} /> </Stack> // 用 Link 或 router 触发 <Link href="/login">登录</Link>
嵌套 Stack
app/
├── _layout.tsx ← Root Stack(管理全局模态)
├── index.tsx
└── account/
├── _layout.tsx ← Account Stack(账户中心子栈)
├── index.tsx
├── profile.tsx
└── security.tsx
// app/account/_layout.tsx import { Stack } from 'expo-router'; export default function AccountLayout() { return ( <Stack screenOptions={{ headerStyle: { backgroundColor: '#0F0A2E' }, headerTintColor: '#fff', }} /> ); }
账户中心的头部样式只作用于 /account/* 下面的屏,外面仍然是 Root 的样式。
动态标题
import { useLocalSearchParams, Stack } from 'expo-router'; export default function Product() { const { id } = useLocalSearchParams(); const product = useProduct(id as string); return ( <> <Stack.Screen options={{ title: product?.name ?? '加载中' }} /> ... </> ); }
去掉 header,自己画
<Stack.Screen options={{ headerShown: false }} /> // 自己的 SafeAreaView + 顶部栏 <SafeAreaView style={{ flex: 1 }}> <MyHeader title="..." onBack={router.back} /> ... </SafeAreaView>
拦截返回
import { useFocusEffect } from 'expo-router'; import { BackHandler, Alert } from 'react-native'; import { useCallback } from 'react'; useFocusEffect( useCallback(() => { const sub = BackHandler.addEventListener('hardwareBackPress', () => { Alert.alert('确认退出?'); return true; // 拦截 }); return () => sub.remove(); }, []) );
路由命名与分组(preview)
文件名用 (group) 包裹表示「路由分组」——URL 路径不含它,但布局可以独享:
app/
├── _layout.tsx ← Root Stack
├── (marketing)/
│ ├── _layout.tsx ← 营销区 Stack(大标题)
│ ├── home.tsx ← /home(不是 /marketing/home)
│ └── about.tsx ← /about
└── (app)/
├── _layout.tsx ← App 区 Stack(小标题)
├── feed.tsx ← /feed
└── profile.tsx ← /profile
下一章讲 Tabs/Drawer 会深入用到分组。
本章小结
- Stack 是原生导航的 JS 表达,底层 @react-navigation/native-stack
- 每屏 options 覆盖 _layout 的 screenOptions(后者是默认)
- presentation: modal / transparentModal 做弹窗,不用自己管 visible
- 嵌套 Stack 让子区域自带头部样式与深度
- 动态 title 放屏幕内,用 useLocalSearchParams 读数据后渲染