Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.

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).

Thanks for reading!

Donate to the site — we work hard for you.