Route Parameters
Dynamic segments, reading and typing params, and why every value arrives as a string no matter what you passed.
const { id } = useLocalSearchParams<{ id: string }>()A dynamic segment is a file whose name is in square brackets. app/order/[id].tsx matches /order/1042 and /order/abc, and the matched text arrives as a param named id. A catch-all, [...rest].tsx, matches the remaining segments instead of one.
Reading them
useLocalSearchParams returns the params of the route the component belongs to, and updates only while that route is focused. useGlobalSearchParams returns the params of whatever route is selected anywhere and updates even when your screen is in the background; reach for it rarely, because it re-renders screens the user is not looking at. Both accept a generic that names the params, as in useLocalSearchParams<{ id: string }>(). router.setParams rewrites the current route's params without pushing a screen.
Everything is a string
Params travel as text in a path or a query string. A number you passed comes back as '1042', a boolean as 'true', and a catch-all as an array of strings. The generic describes what you expect; it converts nothing. Parse once, at the top of the screen, then work with real values.
Pass an id, not an object
It is tempting to pass the whole row the user tapped. Do not. Anything that is not a string is stringified into the URL, nested structure does not survive, and the copy you passed is frozen at the moment of the tap. Pass the identifier and let the detail screen look the record up.
Example
// app/order/[id].tsx - matches /order/1042, /order/abc, any single segment
// On screen: "Order #1042" as a heading, with the referrer underneath when one
// was passed as a query param, or a plain error line when the id is not a number.
import { useLocalSearchParams } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';
export default function OrderScreen() {
// The generic names the params. Every value still arrives as a string.
const { id, ref } = useLocalSearchParams<{ id: string; ref?: string }>();
const orderId = Number(id); // parse; do not trust the declared type
if (!Number.isInteger(orderId)) {
return (
<View style={styles.container}>
<Text style={styles.error}>Not a valid order id: {id}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Order #{orderId}</Text>
{/* A string must be inside a Text. The ternary returns null, not '', so
nothing is rendered when ref is missing. */}
{ref ? <Text style={styles.meta}>Opened from {ref}</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, gap: 8 },
title: { fontSize: 22, fontWeight: '600' },
meta: { fontSize: 14, color: '#687076' },
error: { fontSize: 16, color: '#b3261e' },
});When to use it
- Opening a detail screen for the record a user tapped in a list.
- Accepting an invite code or a share link that carries an id in the path.
- Keeping a filter or search term in the route so the screen can be linked to directly.
More examples
Passing an id from a list
The href object form keeps the route pattern visible, so a rename of the file is a one-line change rather than a hunt through string concatenation. Both rows go to the same screen.
// app/index.tsx
import { Link, useRouter } from 'expo-router';
import { FlatList, Pressable, StyleSheet, Text } from 'react-native';
type Order = { id: string; customer: string };
const ORDERS: Order[] = [
{ id: '1042', customer: 'Ada' },
{ id: '1043', customer: 'Grace' },
];
export default function Index() {
const router = useRouter();
return (
<FlatList
data={ORDERS}
keyExtractor={(order) => order.id}
renderItem={({ item }) => {
const { id } = item;
return (
<Pressable
style={styles.row}
// { id } is the shorthand for { id: id }. The same object.
// Only the id goes across; the detail screen looks the order up.
onPress={() => router.push({ pathname: '/order/[id]', params: { id } })}
>
<Text>{item.customer}</Text>
</Pressable>
);
}}
ListFooterComponent={
<Link href={{ pathname: '/order/[id]', params: { id: '1042' } }} style={styles.row}>
Open 1042 with a Link
</Link>
}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16 },
});Narrowing params once, in one place
Every screen that reads an id needs this logic, so it belongs in one module rather than in each screen. It also makes the three failure cases explicit: missing, not a number, and an array from a catch-all route.
// lib/params.ts - a param is a string, an array of strings, or missing
export function first(value: string | string[] | undefined): string | undefined {
// A catch-all segment arrives as an array; a normal segment does not.
return Array.isArray(value) ? value[0] : value;
}
export function toId(value: string | string[] | undefined): number | null {
const raw = first(value);
if (raw === undefined) return null;
const parsed = Number(raw);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}
console.log(toId('1042'));
// Output: 1042
console.log(toId(['7', '8']));
// Output: 7
console.log(toId('abc'));
// Output: null
console.log(toId(undefined));
// Output: nullKeeping a search term in the route
Putting the query in the params makes the screen shareable as a deep link and survivable across a state restore. Use setParams rather than push whenever the change refines the current screen instead of opening a new one.
// app/search.tsx - the query lives in the route's params, not in local state
import { useLocalSearchParams, useRouter } from 'expo-router';
import { StyleSheet, Text, TextInput, View } from 'react-native';
export default function Search() {
// A destructuring default fills in the missing case.
const { q = '' } = useLocalSearchParams<{ q?: string }>();
const router = useRouter();
return (
<View style={styles.container}>
<TextInput
value={q}
placeholder="Search orders"
autoCapitalize="none"
// setParams rewrites this route's params without pushing a screen,
// so the back gesture is not filled with one entry per keystroke.
onChangeText={(text) => router.setParams({ q: text })}
style={styles.input}
/>
<Text style={styles.hint}>Showing results for: {q === '' ? 'everything' : q}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 12 },
input: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 8, padding: 12 },
hint: { color: '#687076' },
});
Discussion