Dynamic screens and parameters
Each blog post has its own screen, but writing a file for every post makes no sense. A dynamic route is one file with a parameter in square brackets, for example [id].tsx. The parameter value comes from the address: /posts/7 opens this screen with id equal to 7.
Sample data
For now the posts live in a plain array; in the next chapter they are replaced by loading from an API. Create a data folder and the file:
// data/posts.ts
export type Post = { id: number; title: string; body: string };
export const posts: Post[] = [
{ id: 1, title: 'First post', body: 'The text of the first post.' },
{ id: 2, title: 'Second post', body: 'The text of the second post.' },
{ id: 3, title: 'Third post', body: 'The text of the third post.' },
];The post screen
Create an app/posts folder with a [id].tsx file inside; the square brackets are part of the file name:
// app/posts/[id].tsx
import { Stack, useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
import { posts } from '@/data/posts';
export default function PostScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const post = posts.find((p) => p.id === Number(id));
if (!post) {
return <Text>Post not found</Text>;
}
return (
<View style={{ flex: 1, padding: 16, gap: 12 }}>
<Stack.Screen options={{ title: post.title }} />
<Text style={{ fontSize: 22, fontWeight: 'bold' }}>{post.title}</Text>
<Text style={{ fontSize: 16 }}>{post.body}</Text>
</View>
);
}useLocalSearchParams returns the parameters of the current screen. The values are always strings, so id is converted to a number before comparing.
Links to posts
On the main screen, render the list of posts with links:
// app/index.tsx
import { Link } from 'expo-router';
import { FlatList, Text } from 'react-native';
import { posts } from '@/data/posts';
export default function Index() {
return (
<FlatList
data={posts}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<Link
href={{ pathname: '/posts/[id]', params: { id: item.id } }}
style={{ padding: 16, fontSize: 18 }}
>
{item.title}
</Link>
)}
/>
);
}Tap "Second post" and its screen opens with the title in the header. An href object with pathname and params is more convenient than building a string; a string like '/posts/' + item.id works too.
Parameters from code
With useRouter, parameters are passed the same way:
// app/index.tsx
router.push({ pathname: '/posts/[id]', params: { id: 2 } });Extra parameters that are not in the file name go into the address as a query string: params: { id: 2, from: 'home' } gives /posts/2?from=home, and useLocalSearchParams returns both.
Common mistake
Comparing the parameter with a number without converting it: posts.find((p) => p.id === id). The parameter is the string '2' and p.id is the number 2, so strict comparison is always false and the screen shows "Post not found". Convert the value: Number(id).