Build a Multi-Screen Mobile App

Build a multi-screen iOS app with tab navigation using Expo Router.

Introduction

30 Second Summary

Every app on your phone uses multiple screens. Whether you're switching between tabs in a social media app or tapping into a detail page, screen navigation is fundamental to how mobile apps work.

In this project, you will build a multi-screen mobile app called "Daily Discoveries" using Expo and React Native. You'll create tab navigation, a scrollable list of topics, and a dynamic detail screen that changes content based on which topic you tap.

What You'll Build

A React Native app with three tabs (Home, Favorites, About) and a dynamic route that displays topic-specific content based on URL parameters.

By the end of this project, you'll have:

  • A complete Expo project with SDK 56 and TypeScript running natively on the iOS Simulator.
  • Three-tab navigation built with Expo Router's file-based routing system, where files become screens automatically.
  • A dynamic route that renders different content based on URL parameters.
  • Secret Mission: Add a real-time search filter that narrows down topics as you type.

Are there any prerequisites?

This project assumes basic familiarity with JavaScript/TypeScript and React concepts like components and props. You do not need any prior mobile development experience.

Set Up Your Development Environment

Before you can build a mobile app, you need the right tools on your machine. Expo requires Node.js to run its build system and Xcode to power the iOS Simulator where you will preview your app.

In this step, you will install both tools and then scaffold a brand-new Expo project that is ready for development.

In this step, get ready to:
  • Install and verify Node.js.
  • Install Xcode and confirm the command-line tools are configured.
  • Create a new Expo project and clear the boilerplate.
Install Node.js

Expo SDK 56 requires Node.js version 22.13 or higher to run. Let's check whether you already have a compatible version.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Check your Node.js version by running this command:
node --version

✔️ I see version 22 or higher

Your Node.js version is compatible with Expo SDK 56. Move on to the next substep.

ⓧ I see an older version

You have Node.js installed, but it is below the required 22.13 minimum. You can upgrade using nvm (Node Version Manager) or by downloading the latest LTS installer from the official site.

  • If you have nvm installed, upgrade by running:
  • If you prefer a manual install, visit https://nodejs.org and download the LTS installer for your platform.
nvm install 22
nvm use 22
  • After upgrading, verify again by running:
node --version

You should see v22.x.x or higher.

ⓧ Command not found

Node.js is not installed yet. The fastest way to install it is through the official installer.

  • Visit https://nodejs.org and download the LTS installer for your platform.
  • Run the installer and accept the default options.
  • Close and reopen your terminal, then verify by running:
node --version

You should see v22.x.x or higher printed in your terminal.

Install Xcode and configure the command-line tools

The iOS Simulator is bundled inside Xcode. You need the full Xcode app (not just the command-line tools) so that Expo can launch and run your app in the simulator.

  • Open the Mac App Store and search for Xcode.
  • Click Get (or Update if you already have an older version) to install it.
  • Once the install finishes, open Xcode at least once and accept the license agreement.

Why the full Xcode app?

Expo's iOS Simulator support depends on simulator runtimes that ship inside Xcode. The standalone command-line tools package does not include these runtimes.

Now confirm the command-line tools are pointing at your Xcode installation.

  • In your terminal, verify the active developer directory by running:
xcode-select -p

✔️ I see a path like /Applications/Xcode.app/Contents/Developer

Your command-line tools are configured correctly. Move on to the next substep.

ⓧ I see an error or a different path

The tools are not pointing at Xcode. Fix this by running the following command in your terminal:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
  • Enter your Mac password when prompted.
  • Run xcode-select -p again to confirm it now shows /Applications/Xcode.app/Contents/Developer.
Create the Expo project

With Node.js and Xcode ready, you can scaffold a new Expo project. Expo's create command generates a full TypeScript project with file-based routing already wired up.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project by running this command:
npx create-expo-app@latest DailyDiscoveries --template default@sdk-56

What does this command do?

npx create-expo-app@latest downloads and runs the latest version of Expo's project scaffolding tool.

DailyDiscoveries is the folder name for your new project. The --template default@sdk-56 flag ensures you get Expo SDK 56 with TypeScript and file-based routing preconfigured.

  • Once the scaffolding finishes, move into the project folder by running:
cd DailyDiscoveries

The default template includes demo screens and components you do not need. Expo provides a reset script that clears this boilerplate.

  • Run the reset-project script to clear the boilerplate:
npm run reset-project

What does the reset script do?

This script moves the original demo files into an app-example folder and leaves you with a clean app/index.tsx and app/_layout.tsx to build from.

  • Confirm the project files exist by running:
ls src/app

You should see index.tsx and _layout.tsx listed in the output.

Don't see src/app?

Make sure you ran npm run reset-project from inside the DailyDiscoveries folder. If you are in a different directory, run cd ~/Desktop/DailyDiscoveries first, then try the reset script again.

Help me troubleshoot my Expo project structure after running the reset script.

Finally, open the project in VS Code so you are ready to edit files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the DailyDiscoveries folder, and click Open.

Your development environment is ready. Next up, you will explore the project structure and launch the app in the iOS Simulator for the first time.

Explore the Project Structure and Launch the App

Your project is created and the boilerplate is cleared. Before writing any new code, it helps to understand how Expo Router organizes your app's screens.

Expo Router uses file-based routing. Every file you place inside the src/app/ directory automatically becomes a screen in your app. In this step, you will explore those files, launch the app in the iOS Simulator, and confirm that changes you make appear instantly.

In this step, get ready to:
  • Explore the file structure inside src/app/.
  • Start the development server and open the iOS Simulator.
  • Confirm hot reload is working by editing a file.
Explore the generated file structure

Your project already has three key files inside src/app/. Each one plays a distinct role in how your app is structured and navigated.

  • In VS Code, look at the Explorer sidebar on the left.
  • Expand the src folder, then expand the app folder inside it.
  • You should see three files: _layout.tsx, index.tsx, and explore.tsx.

What does each file do?

  • _layout.tsx is the root layout. It defines how all screens in the directory are arranged (tabs, stacks, etc.). Think of it as the frame around your pages.
  • index.tsx is the home screen. Expo Router looks for the first index.tsx matching the / URL and displays it when the app opens.
  • explore.tsx is a second screen. Its filename becomes the route /explore automatically.

Why file-based routing?

In traditional React Native projects you have to manually wire up every screen in a navigation configuration file. With file-based routing, adding a new screen is as simple as creating a new file. The file system IS your navigation map.

Start the development server and open the iOS Simulator

The Expo development server watches your files and pushes changes to connected devices in real time. You will launch it and connect it to the iOS Simulator.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Start the development server by running this command:
npx expo start

After the server starts, you will see a QR code and a list of keyboard shortcuts in the terminal.

  • Press i in the terminal to open the iOS Simulator.

What is happening when you press i?

Expo CLI launches the iOS Simulator (provided by Xcode) and installs a development client on it. The client connects to your local dev server so it can display your app and receive live updates.

Wait for the simulator to boot and the app to load. You should see the default home screen with text displayed in the center.

Simulator not launching?

  • Make sure Xcode is fully installed and you accepted the license agreement. Run sudo xcodebuild -license accept if prompted.
  • Check that the Xcode command-line tools are pointed at your Xcode installation. Run xcode-select -p and verify it returns a path inside your Xcode app.
  • If the simulator opens but the app does not load, try pressing i again in the terminal.

help me get the iOS Simulator working with Expo

Verify hot reload by editing the home screen

Hot reload means your changes appear in the simulator the moment you save a file. No manual refresh or rebuild is needed. Let's confirm it works.

  • In VS Code, click on src/app/index.tsx in the Explorer sidebar to open it.
  • Find the text that is displayed on screen (inside the <Text> component).
  • Change it to Hello World! (or any text you like).
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

Look at the iOS Simulator. Your new text should appear within a second or two without restarting the app.

Why is this useful?

Hot reload keeps your app running while injecting updated code. You keep your navigation state, form inputs, and scroll position. This makes the development loop extremely fast compared to a full rebuild.

Changes not showing up?

  • Make sure you saved the file. Unsaved changes will not trigger a reload.
  • Check the terminal for error messages. A syntax error in your edit will prevent the reload from completing.
  • If the app is still stuck, press r in the terminal to force a full reload.

help me fix hot reload in Expo

Your development environment is fully wired up. You can write code, save, and see results instantly. Next up, you will build the tab navigation that gives your app its multi-screen structure.

Build the Tab Navigation

Your app is running on the Simulator and hot reload is working. Right now it only shows a single screen with placeholder content.

Mobile apps need multiple screens with easy navigation between them. In this step, you'll build a three-tab navigation bar using Expo Router's Tabs component that lets users switch between Home, Favorites, and About screens with a single tap.

In this step, get ready to:
  • Configure src/app/_layout.tsx with a Tabs navigator.
  • Create the Favorites and About tab screens.
  • Verify all three tabs appear in the iOS Simulator.
Configure the tab layout

The _layout.tsx file controls how screens in its directory are arranged. You'll replace the existing layout with a Tabs navigator that creates a bottom tab bar.

  • First, delete the explore.tsx file that came with the template. You no longer need it since you'll create your own screens. In VS Code's Explorer sidebar, right-click src/app/explore.tsx and select Delete.
  • Now open src/app/_layout.tsx in the editor.
  • Select all the existing content and replace it with the following code:
import { Tabs } from 'expo-router';

export default function RootLayout() {
  return (
    <Tabs
      screenOptions={{
        // Highlight the active tab in indigo
        tabBarActiveTintColor: '#6366f1',
        // Show a header bar at the top of each screen
        headerShown: true,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarLabel: 'Home',
        }}
      />
      <Tabs.Screen
        name="favorites"
        options={{
          title: 'Favorites',
          tabBarLabel: 'Favorites',
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: 'About',
          tabBarLabel: 'About',
        }}
      />
      <Tabs.Screen
        name="topic/[id]"
        options={{
          // Hide this route from the tab bar
          href: null,
        }}
      />
    </Tabs>
  );
}

What does this code do?

The Tabs component from expo-router creates a bottom tab bar navigator. Each Tabs.Screen entry declares a route that appears as a tab.

  • screenOptions sets global options for all tabs. tabBarActiveTintColor colors the active tab indigo, and headerShown: true displays a title bar at the top of each screen.
  • The name property on each Tabs.Screen matches the filename (without the extension) in src/app/. The title sets the header text, and tabBarLabel sets the label shown beneath each tab icon.
  • The topic/[id] entry with href: null hides that route from the tab bar. You'll build this dynamic route in a later step, but declaring it now prevents errors.

Your app will show an error in the Simulator because the favorites and about screens don't exist yet. You'll create them next.

Create the Favorites screen

Each tab needs a corresponding file in src/app/. This is file-based routing in action. The filename becomes the route name automatically.

  • Create a new file called favorites.tsx inside src/app/. In VS Code's Explorer sidebar, right-click the src/app folder and select New File.
  • Name it favorites.tsx and paste the following code:
import { StyleSheet, Text, View } from 'react-native';

export default function FavoritesScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>Favorites</Text>
      <Text style={styles.placeholder}>
        Your bookmarked topics will appear here.
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
  },
  placeholder: {
    fontSize: 16,
    color: '#64748b',
    textAlign: 'center',
  },
});

What does this code do?

This is a simple placeholder screen using React Native components. View is like a div in web development. Text renders text on screen.

The StyleSheet.create() method defines styles in a way that React Native optimizes for performance. Instead of CSS files, React Native uses JavaScript objects with camelCase properties like backgroundColor and justifyContent.

  • Save favorites.tsx.
  • In the Simulator, tap the Favorites tab at the bottom. You should see the heading "Favorites" and the placeholder text centered on screen.

Don't see the Favorites tab?

  • Make sure the file is named exactly favorites.tsx (all lowercase) and is saved inside src/app/, not a subfolder.
  • Check that you deleted explore.tsx earlier. Leftover files can create unexpected tabs.
  • If the app shows a red error screen, check _layout.tsx for typos in the name properties. They must match filenames exactly.

My Favorites tab isn't showing up after saving the file. Can you help me debug?

Create the About screen
  • Create another new file called about.tsx inside src/app/. Right-click the src/app folder in the Explorer sidebar and select New File.
  • Name it about.tsx and paste the following code:
import { StyleSheet, Text, View } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>About Daily Discoveries</Text>
      <Text style={styles.body}>
        Daily Discoveries is a learning app built with Expo and React Native.
        It demonstrates file-based routing, tab navigation, and dynamic routes.
      </Text>
      <Text style={styles.version}>Built with Expo SDK 56</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  body: {
    fontSize: 16,
    color: '#475569',
    textAlign: 'center',
    lineHeight: 24,
    marginBottom: 24,
  },
  version: {
    fontSize: 14,
    color: '#94a3b8',
  },
});

What does this code do?

This screen displays information about the app. It uses the same pattern as the Favorites screen: a View container with Text children and a StyleSheet for layout.

The flexbox properties alignItems: 'center' and justifyContent: 'center' center the content both horizontally and vertically. flex: 1 makes the container fill the entire screen.

  • Save about.tsx.
  • In the Simulator, tap the About tab. You should see "About Daily Discoveries" as the heading, a description paragraph, and "Built with Expo SDK 56" at the bottom.
  • Tap through all three tabs (Home, Favorites, About) to confirm they all work. The active tab should highlight in indigo.

Tabs not switching correctly?

  • Make sure both favorites.tsx and about.tsx are saved inside src/app/ (the same folder as index.tsx and _layout.tsx).
  • Verify the name values in _layout.tsx match the filenames exactly: index, favorites, about.
  • If the app is stuck, press r in the terminal running the dev server to reload the app.

My tabs aren't switching or the app crashes when I tap a tab. Help me fix this.

Why file-based routing?

In traditional React Native apps, you configure navigation in JavaScript code, mapping route names to components manually. Expo Router flips this around.

Each file you create in src/app/ automatically becomes a route. The _layout.tsx file defines how those routes are presented (as tabs, stacks, or drawers). This means adding a new screen is as simple as creating a new file.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at this point:

import { Tabs } from 'expo-router';

export default function RootLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#6366f1',
        headerShown: true,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarLabel: 'Home',
        }}
      />
      <Tabs.Screen
        name="favorites"
        options={{
          title: 'Favorites',
          tabBarLabel: 'Favorites',
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: 'About',
          tabBarLabel: 'About',
        }}
      />
      <Tabs.Screen
        name="topic/[id]"
        options={{
          href: null,
        }}
      />
    </Tabs>
  );
}
import { StyleSheet, Text, View } from 'react-native';

export default function FavoritesScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>Favorites</Text>
      <Text style={styles.placeholder}>
        Your bookmarked topics will appear here.
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
  },
  placeholder: {
    fontSize: 16,
    color: '#64748b',
    textAlign: 'center',
  },
});
import { StyleSheet, Text, View } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>About Daily Discoveries</Text>
      <Text style={styles.body}>
        Daily Discoveries is a learning app built with Expo and React Native.
        It demonstrates file-based routing, tab navigation, and dynamic routes.
      </Text>
      <Text style={styles.version}>Built with Expo SDK 56</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  body: {
    fontSize: 16,
    color: '#475569',
    textAlign: 'center',
    lineHeight: 24,
    marginBottom: 24,
  },
  version: {
    fontSize: 14,
    color: '#94a3b8',
  },
});

Your app now has a fully working three-tab navigation bar. Next up, you'll build the Home screen with a scrollable list of topics that users can tap to explore.

Build the Home Screen with a Topics List

Your three-tab layout is working. Tapping Home, Favorites, and About each loads a different screen.

Now it is time to make the Home screen actually useful. You will build a scrollable list of topics that users can browse and tap to learn more.

In this step, get ready to:
  • Replace the Home screen with a scrollable list of topics using FlatList.
  • Style each topic card with rounded corners, shadows, and spacing.
  • Link each topic to a detail page using Expo Router.
Set up the topics data

The Home screen needs a list of topics to display. You will define this data as a simple array of objects, each with an id, title, and description.

  • Open src/app/index.tsx in VS Code.
  • Select all the existing content and delete it.
  • Add the following imports and topics data at the top of the file:
import { StyleSheet, Text, View, FlatList, Pressable } from 'react-native';
import { Link } from 'expo-router';

// Topic data that populates the Home screen list
const topics = [
  { id: '1', title: 'The Solar System', description: 'Explore planets, moons, and asteroids.' },
  { id: '2', title: 'Ocean Life', description: 'Discover creatures of the deep sea.' },
  { id: '3', title: 'Ancient Civilizations', description: 'Learn about history makers.' },
  { id: '4', title: 'Space Exploration', description: 'Rockets, rovers, and the final frontier.' },
  { id: '5', title: 'Rainforest Ecosystems', description: 'The lungs of our planet.' },
];

What does this code do?

The first import pulls in React Native components you will use: FlatList for the scrollable list and Pressable to make each card tappable.

The second import brings in Link from expo-router, which handles navigation to a detail page when the user taps a topic.

The topics array holds five objects. Each has a unique id that the detail screen will use later to show the right content.

You won't see a change yet. The next chunk builds the component that renders this data on screen.

Build the scrollable list with navigation links

FlatList is React Native's high-performance scrollable list. It only renders items visible on screen, which keeps your app fast even with large data sets.

  • Add the HomeScreen component below the topics array by pasting this code:
export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>Daily Discoveries</Text>
      <FlatList
        data={topics}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
        renderItem={({ item }) => (
          // Link wraps each card to navigate to /topic/[id]
          <Link href={`/topic/${item.id}`} asChild>
            <Pressable style={styles.card}>
              <Text style={styles.cardTitle}>{item.title}</Text>
              <Text style={styles.cardDescription}>{item.description}</Text>
            </Pressable>
          </Link>
        )}
      />
    </View>
  );
}

What does this code do?

  • FlatList receives the topics array via its data prop and renders one card per item.
  • keyExtractor gives each item a stable identity so React knows which items changed when the list updates.
  • Link with asChild passes the navigation behaviour down to Pressable. Tapping a card navigates to /topic/1, /topic/2, etc.

What is file-based routing?

Expo Router uses file-based routing. Every file inside src/app/ automatically becomes a route in your app.

The href in the Link points to /topic/[id]. In the next step, you will create a file at that path so tapping a card opens the matching detail screen.

You won't see a change yet. The component references styles that do not exist yet, so the file would error if saved now. The next chunk adds those styles.

Style the topic cards

React Native does not use CSS files. Instead, you define styles with the StyleSheet API, which keeps your layout fast and co-located with the component.

  • Add the styles below the HomeScreen function by pasting this code:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    paddingTop: 16,
  },
  heading: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1e293b',
    paddingHorizontal: 16,
    marginBottom: 16,
  },
  list: {
    paddingHorizontal: 16,
    paddingBottom: 16,
  },
  // Card styles: white background, rounded corners, subtle shadow
  card: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
    elevation: 2,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1e293b',
    marginBottom: 4,
  },
  cardDescription: {
    fontSize: 14,
    color: '#64748b',
  },
});

What does this code do?

  • container fills the screen (flex: 1) and sets a light grey background.
  • card gives each topic a white background, borderRadius: 12 for rounded corners, and a subtle shadow for depth.
  • elevation: 2 adds shadow on Android. The shadowColor and shadowOffset properties handle iOS shadows.
  • Save src/app/index.tsx.
  • Switch to the iOS Simulator. You should see the Home tab with a "Daily Discoveries" heading and five topic cards in a scrollable list.
  • Scroll down to confirm all five topics are visible.
  • Tap any topic card. The app will try to navigate but show a blank screen because the detail route does not exist yet. That is expected.

Don't see the topic cards?

  • Make sure you deleted all the old content before pasting the new code. Leftover code from the previous version of index.tsx will cause conflicts.
  • Check for red error text in the Simulator. A missing comma or bracket in the topics array or styles object is a common cause.
  • Verify that the file saved. If hot reload does not trigger, press r in the terminal running the dev server to force a reload.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/app/index.tsx file for comparison:

import { StyleSheet, Text, View, FlatList, Pressable } from 'react-native';
import { Link } from 'expo-router';

const topics = [
  { id: '1', title: 'The Solar System', description: 'Explore planets, moons, and asteroids.' },
  { id: '2', title: 'Ocean Life', description: 'Discover creatures of the deep sea.' },
  { id: '3', title: 'Ancient Civilizations', description: 'Learn about history makers.' },
  { id: '4', title: 'Space Exploration', description: 'Rockets, rovers, and the final frontier.' },
  { id: '5', title: 'Rainforest Ecosystems', description: 'The lungs of our planet.' },
];

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.heading}>Daily Discoveries</Text>
      <FlatList
        data={topics}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
        renderItem={({ item }) => (
          <Link href={`/topic/${item.id}`} asChild>
            <Pressable style={styles.card}>
              <Text style={styles.cardTitle}>{item.title}</Text>
              <Text style={styles.cardDescription}>{item.description}</Text>
            </Pressable>
          </Link>
        )}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    paddingTop: 16,
  },
  heading: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1e293b',
    paddingHorizontal: 16,
    marginBottom: 16,
  },
  list: {
    paddingHorizontal: 16,
    paddingBottom: 16,
  },
  card: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
    elevation: 2,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1e293b',
    marginBottom: 4,
  },
  cardDescription: {
    fontSize: 14,
    color: '#64748b',
  },
});

Your Home screen is alive with tappable content. Next up, you will create the dynamic detail screen so each topic card actually opens something meaningful.

Create a Dynamic Detail Screen

Your Home screen now shows a polished list of topic cards, each wired up to navigate to /topic/[id]. Right now, tapping a card takes you to a blank screen because no file exists at that path yet.

In this step, you'll create a dynamic route that catches every /topic/... URL and renders content specific to whichever topic was tapped.

In this step, get ready to:
  • Create the dynamic route file that responds to any topic ID in the URL.
  • Read the URL parameter with the useLocalSearchParams hook.
  • Build a styled detail screen with a dynamic header title.
Create the dynamic route file

Expo Router uses square brackets in filenames to create dynamic routes. A file named [id].tsx inside a folder captures any value at that URL segment and makes it available as a parameter.

  • In VS Code's Explorer sidebar, right-click the src/app folder.
  • Click New Folder and name it topic.
  • Right-click the new topic folder, click New File, and name it [id].tsx.
  • Add the imports and topic data by pasting this code into src/app/topic/[id].tsx:
import { StyleSheet, Text, View } from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';

// Data for each topic, keyed by ID
const topicDetails: Record<string, { title: string; content: string }> = {
  '1': {
    title: 'The Solar System',
    content: 'Our solar system has eight planets orbiting the Sun. From the rocky inner planets to the gas giants, each world is unique. Mercury is the smallest, Jupiter the largest, and Earth is the only one known to harbor life.',
  },
  '2': {
    title: 'Ocean Life',
    content: 'The ocean covers over 70% of Earth and contains millions of species. From microscopic plankton to the blue whale, marine life is incredibly diverse. The deep sea remains one of the least explored areas on our planet.',
  },
  '3': {
    title: 'Ancient Civilizations',
    content: 'From the pyramids of Egypt to the temples of Rome, ancient civilizations shaped the modern world. They invented writing, mathematics, and governance systems that we still use today.',
  },
  '4': {
    title: 'Space Exploration',
    content: 'Humans first reached space in 1961. Since then, we have landed on the Moon, sent rovers to Mars, and launched telescopes that peer into the depths of the universe.',
  },
  '5': {
    title: 'Rainforest Ecosystems',
    content: 'Tropical rainforests are home to over half of all plant and animal species. They produce oxygen, regulate climate, and provide resources for millions of people worldwide.',
  },
};

What does this code do?

The imports pull in useLocalSearchParams and Stack from expo-router. These let you read URL parameters and control the navigation header from inside the screen.

The topicDetails object maps each topic ID (like '1' or '3') to its title and body content. When a user taps a card, the component looks up the matching entry.

What is a dynamic route?

In file-based routing, square brackets in a filename create a wildcard segment. The file [id].tsx matches any URL like /topic/1, /topic/2, or /topic/42.

The value in that URL position becomes available as a parameter named id inside the component. One file handles unlimited variations of the same screen.

  • Save src/app/topic/[id].tsx.

You won't see a change in the simulator yet. The next chunk adds the component that renders the screen.

Build the TopicDetail component

The useLocalSearchParams hook reads whatever value landed in the [id] segment of the URL. You'll use that value to look up the right topic and display its content.

  • Add the component function below the topicDetails object in src/app/topic/[id].tsx:
export default function TopicDetail() {
  // Read the id parameter from the URL
  const { id } = useLocalSearchParams<{ id: string }>();
  // Look up the topic, with a fallback for unknown IDs
  const topic = topicDetails[id] || { title: 'Not Found', content: 'This topic does not exist.' };

  return (
    <>
      {/* Dynamically set the header title to the topic name */}
      <Stack.Screen options={{ title: topic.title }} />
      <View style={styles.container}>
        <Text style={styles.title}>{topic.title}</Text>
        <Text style={styles.content}>{topic.content}</Text>
      </View>
    </>
  );
}

What does this component do?

  • useLocalSearchParams<{ id: string }>() extracts the id value from the current URL. If the user navigated to /topic/3, id equals '3'.
  • The fallback object ({ title: 'Not Found', ... }) handles edge cases where the ID doesn't match any topic.
  • Stack.Screen options={{ title: topic.title }} sets the navigation bar title dynamically so each topic shows its own name in the header.
  • Save src/app/topic/[id].tsx.

You won't see the screen render correctly yet because the styles haven't been defined. The next chunk adds them.

Style the detail screen
  • Add the StyleSheet at the bottom of src/app/topic/[id].tsx, below the component function:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    padding: 24,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 16,
  },
  content: {
    fontSize: 16,
    color: '#475569',
    lineHeight: 26,
  },
});

What does this code do?

  • container fills the screen with a light background and consistent padding around all content.
  • title uses a large bold font so the topic name stands out at the top of the screen.
  • content sets comfortable reading typography with a generous lineHeight of 26 for readability.
  • Save src/app/topic/[id].tsx.
  • Switch to the iOS Simulator and tap any topic card on the Home tab.
  • You should see a detail screen with the topic's title in the header, a large heading, and a paragraph of content below it.
  • Tap the back arrow in the top-left corner to return to the Home screen.

Seeing a blank screen or error?

  • Make sure the file is at src/app/topic/[id].tsx with square brackets in the filename. The brackets are literal characters, not a placeholder.
  • Check that the folder is named topic (not topics). The Link in your Home screen points to /topic/${item.id} so the folder name must match exactly.
  • If the simulator seems stuck, press r in the terminal running the dev server to reload the app.

Why isn't my dynamic route screen loading when I tap a topic card?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete src/app/topic/[id].tsx file:

import { StyleSheet, Text, View } from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';

const topicDetails: Record<string, { title: string; content: string }> = {
  '1': {
    title: 'The Solar System',
    content: 'Our solar system has eight planets orbiting the Sun. From the rocky inner planets to the gas giants, each world is unique. Mercury is the smallest, Jupiter the largest, and Earth is the only one known to harbor life.',
  },
  '2': {
    title: 'Ocean Life',
    content: 'The ocean covers over 70% of Earth and contains millions of species. From microscopic plankton to the blue whale, marine life is incredibly diverse. The deep sea remains one of the least explored areas on our planet.',
  },
  '3': {
    title: 'Ancient Civilizations',
    content: 'From the pyramids of Egypt to the temples of Rome, ancient civilizations shaped the modern world. They invented writing, mathematics, and governance systems that we still use today.',
  },
  '4': {
    title: 'Space Exploration',
    content: 'Humans first reached space in 1961. Since then, we have landed on the Moon, sent rovers to Mars, and launched telescopes that peer into the depths of the universe.',
  },
  '5': {
    title: 'Rainforest Ecosystems',
    content: 'Tropical rainforests are home to over half of all plant and animal species. They produce oxygen, regulate climate, and provide resources for millions of people worldwide.',
  },
};

export default function TopicDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const topic = topicDetails[id] || { title: 'Not Found', content: 'This topic does not exist.' };

  return (
    <>
      <Stack.Screen options={{ title: topic.title }} />
      <View style={styles.container}>
        <Text style={styles.title}>{topic.title}</Text>
        <Text style={styles.content}>{topic.content}</Text>
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    padding: 24,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 16,
  },
  content: {
    fontSize: 16,
    color: '#475569',
    lineHeight: 26,
  },
});

Secret mission

Add Real-Time Search Filtering

Your Home screen shows five topics, but what if there were hundreds? Add a search bar that filters topics in real time as the user types. You'll use React's useState hook and JavaScript's filter method to make the list respond instantly to keyboard input.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The DailyDiscoveries project folder on your Desktop.
  • The Expo development server (running in your terminal).

Keep everything running

No action needed. Choose this if you're still actively building or want to pick up where you left off next time.

  • Your DailyDiscoveries folder stays on your Desktop for future development.
  • The Expo dev server will stop automatically when you close the terminal window.

Pause - I'll come back to this later

Stop the running dev server to free up system resources. Your project files stay on disk so you can resume any time.

  • Switch to the terminal where Expo is running and press Ctrl+C to stop the dev server.
  • Close the iOS Simulator window.
  • When you're ready to resume, open a terminal in the DailyDiscoveries folder and run npx expo start again.

Delete - I don't want to use this again

Remove all project files and stop running processes. This cannot be undone.

  • Switch to the terminal where Expo is running and press Ctrl+C to stop the dev server.
  • Close the iOS Simulator window.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/DailyDiscoveries

On Windows, run this instead:

rmdir /s /q %USERPROFILE%\Desktop\DailyDiscoveries
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or checking your Desktop in File Explorer (Windows). You should no longer see DailyDiscoveries listed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete multi-screen mobile app from scratch using Expo SDK 56 and TypeScript, running natively on the iOS Simulator.

You've learned how to:

  • Built a multi-screen mobile app with three-tab navigation using Expo Router's file-based routing system.
  • Created a dynamic route that renders different content based on URL parameters, using the useLocalSearchParams hook.
  • Styled native mobile components using React Native's StyleSheet API and flexbox layouts.
  • Secret Mission: Added real-time search filtering with React state management.

Ready to quiz yourself?