单段参数 [id]
app/
├── _layout.tsx
├── index.tsx
└── products/
├── index.tsx ← /products
└── [id].tsx ← /products/:id
// app/products/[id].tsx import { useLocalSearchParams, Stack } from 'expo-router'; import { View, Text, ActivityIndicator } from 'react-native'; export default function ProductDetail() { const { id } = useLocalSearchParams<{ id: string }>(); const { data, isLoading } = useProduct(id); if (isLoading) return <ActivityIndicator />; return ( <> <Stack.Screen options={{ title: data.name }} /> <View> <Text>{data.name}</Text> <Text>¥{data.price}</Text> </View> </> ); }
导航过去
// 声明式 <Link href="/products/42">查看商品</Link> // 对象写法,参数更清晰 <Link href={{ pathname: '/products/[id]', params: { id: '42' } }}>查看</Link> // 命令式 router.push({ pathname: '/products/[id]', params: { id: product.id } });
pathname 写 [id] 而不是实际值
对象写法推荐
对象写法推荐
pathname: '/products/[id]',让 Typed Routes 知道这是哪个路由模板,然后在 params 里填具体值。字符串 /products/42 也能跑,但类型推导弱一些。
多段参数:多个 []
app/posts/[category]/[slug].tsx ← /posts/tech/expo-router-v4
const { category, slug } = useLocalSearchParams<{ category: string; slug: string; }>();
Catch-all [...slug]
三点表示「贪婪匹配剩下所有段」,适合文档/层级内容:
app/docs/[...slug].tsx /docs/intro → slug = ['intro'] /docs/api/core/client → slug = ['api', 'core', 'client'] /docs → 不匹配(至少 1 段)
const { slug } = useLocalSearchParams<{ slug: string[] }>(); // slug 是数组
可选 Catch-all [[...slug]]
双方括号表示「零段或多段都可以」,适合首页/空路径:
app/docs/[[...slug]].tsx /docs → slug = undefined /docs/intro → slug = ['intro']
useLocalSearchParams vs useGlobalSearchParams
useLocalSearchParams
只读当前路由段的参数。嵌套路由下,父路由的参数不返回。一般情况下用这个。
useGlobalSearchParams
读整个导航栈里所有路由的参数合集。父路由改了参数,子屏能拿到变化,但会多触发渲染。
// 场景:tab 切换时,当前 tab 的参数保留 // /products/42 下: // useLocalSearchParams() → { id: '42' } // useGlobalSearchParams() → { id: '42', ...父级参数 }
Query 参数(?a=1&b=2)
// /products/42?variant=red&size=L const { id, variant, size } = useLocalSearchParams<{ id: string; variant: string; size: string; }>(); // 导航带 query router.push({ pathname: '/products/[id]', params: { id: '42', variant: 'red', size: 'L' }, });
Expo Router 把 [id] 解析成路径段,剩下没匹配到的 key 当作 query——统一通过 useLocalSearchParams 拿,API 一致。
Typed Routes
// app.json { "expo": { "experiments": { "typedRoutes": true } } }
启用后,Expo 扫描 app/ 目录,生成 .expo/types/router.d.ts——所有 Href 类型都精确到每个可能的路由:
// ✅ 能编译 router.push('/products/42'); router.push({ pathname: '/products/[id]', params: { id: '42' } }); // ❌ TS 报错 router.push('/prodcuts/42'); // 拼错 → 报错 router.push({ pathname: '/products/[id]' }); // 缺 params → 报错
Typed Routes 是"静态的"
它基于文件系统扫描,不能处理运行时拼接的字符串。
它基于文件系统扫描,不能处理运行时拼接的字符串。
router.push(`/products/${id}`) 类型上就退化成 string——把 id 放 params 里才能拿到类型保护。
useSegments:当前路径的段数组
import { useSegments } from 'expo-router'; const segments = useSegments(); // /(tabs)/home/42 → ['(tabs)', 'home', '42'] // 用途:判断当前在哪个分组(/(auth)/* 或 /(app)/*)来做鉴权
usePathname:纯字符串路径
import { usePathname } from 'expo-router'; const pathname = usePathname(); // '/home/42' (不含 query,不含分组)
路由参数变化触发刷新
const { id } = useLocalSearchParams<{ id: string }>(); // 关键:把 id 放 dependency 里,id 变屏幕不卸载,数据会刷新 const { data } = useQuery({ queryKey: ['product', id], queryFn: () => fetchProduct(id), });
Stack 里的 /products/42 → /products/43
默认走 push,相当于栈顶又加一屏(返回能回到 42)。如果你想就地替换,用
默认走 push,相当于栈顶又加一屏(返回能回到 42)。如果你想就地替换,用
router.setParams({ id: '43' }) 或 router.replace。
setParams:更新当前路由参数
import { router } from 'expo-router'; // 筛选条件变化,不跳转,只改 query router.setParams({ variant: 'blue', size: 'M' }); // URL 从 /products/42?variant=red 变成 /products/42?variant=blue&size=M
参数都是 string
无论是 URL 路径还是 query,Expo Router 拿到的永远是 string——即便你 params 传了 number,到下一屏也变字符串。需要自己 parse:
const { id, page } = useLocalSearchParams<{ id: string; page: string }>(); const productId = Number(id); const currentPage = parseInt(page ?? '1', 10);
Not Found 兜底
app/+not-found.tsx
import { Link, Stack } from 'expo-router'; import { View, Text } from 'react-native'; export default function NotFound() { return ( <> <Stack.Screen options={{ title: '走丢了' }} /> <View> <Text>这个页面不存在</Text> <Link href="/">返回首页</Link> </View> </> ); }
完整示例:商品列表 → 详情
// app/products/index.tsx import { FlatList, Text, Pressable } from 'react-native'; import { Link } from 'expo-router'; export default function ProductList() { const { data } = useProducts(); return ( <FlatList data={data} keyExtractor={(p) => p.id} renderItem={({ item }) => ( <Link href={{ pathname: '/products/[id]', params: { id: item.id } }} asChild > <Pressable style={{ padding: 16 }}> <Text>{item.name}</Text> <Text>¥{item.price}</Text> </Pressable> </Link> )} /> ); }
本章小结
[id].tsx单段参数,[...slug].tsx贪婪匹配,[[...slug]].tsx可选- 读参数
useLocalSearchParams<{}>(),Typed Routes 开typedRoutes: true - query 和路径参数合并到同一个对象,key 都是 string,自己 parse 类型
router.setParams改当前 URL 不跳转,适合筛选/排序+not-found.tsx兜底不匹配的路径