Expo Router: Say Goodbye to ‘Manual’ Navigation Worries in React Native

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The Nightmare of Manual Navigation Configuration

If you’ve been struggling with React Native since the early days, you’re definitely familiar with react-navigation. While powerful, manually defining every screen in the App.js file is a real pain. The configuration file grows to hundreds or even thousands of lines. Every time a new feature is added, you have to go back to that file to declare it. It’s incredibly time-consuming and prone to confusion as the project scales.

Everything changed completely with the arrival of Expo Router. It brings the “File-based Routing” philosophy from Next.js to the mobile environment. Simply put: you just create a file in a directory, and Expo automatically understands it as a route. No more manual importing/exporting of Navigators. Here’s how I apply Expo Router to build a standard folder structure for real projects.

Why Expo Router is the New ‘Go-To’ Solution?

Essentially, Expo Router still runs on top of react-navigation but is fully automated. Instead of writing code to define paths, you use your folder structure.

Here are three reasons why I fell for this technology immediately:

  • Native-first: Optimized for smooth performance on both iOS and Android.
  • Default Deep Linking: Configuring Deep Linking usually takes all morning. With Expo Router, the file path is the URL. Everything works out of the box.
  • Intuitive Structure: Just looking at the app/ folder tells you exactly how many screens the app has. Extremely easy to maintain.

Building a Standard Folder Structure for Your Project

First, initialize a new Expo project using the familiar command:

npx create-expo-app@latest my-new-app

In this ecosystem, all navigation logic resides within the app/ folder. Here is the framework I frequently use:

app/
├── (auth)/
│   ├── login.tsx
│   └── register.tsx
├── (tabs)/
│   ├── index.tsx (Home)
│   ├── settings.tsx
│   └── _layout.tsx
├── user/
│   └── [id].tsx (User details page)
├── _layout.tsx (Root layout)
└── +not-found.tsx (404 page)

1. _layout.tsx File – The Application Framework

The _layout.tsx file acts as the “backbone.” It contains shared components like Header, Footer, or Providers.

In the outermost app/_layout.tsx file, I usually set up the main Stack as follows:

import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="(auth)" options={{ presentation: 'modal' }} />
      <Stack.Screen name="user/[id]" options={{ title: 'Personal Information' }} />
    </Stack>
  );
}

2. Route Groups: The Parentheses Trick ()

Ever wonder why I use (auth) or (tabs)? Parentheses help group files neatly without affecting the URL.

For example: The app/(auth)/login.tsx file will have the path /login instead of /auth/login. This keeps the folder structure extremely clean while the URL remains concise and professional.

3. Tabs Layout – Bottom Navigation Bar

To create a Tab bar, I use the app/(tabs)/_layout.tsx file. Expo Router provides a powerful built-in Tabs component:

import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <Ionicons name="home" size={28} color={color} />,
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: 'Settings',
          tabBarIcon: ({ color }) => <Ionicons name="settings" size={28} color={color} />,
        }}
      />
    </Tabs>
  );
}

Dynamic Routes: Handling Detail Pages Flexibly

For pages like product details or personal profiles, simply name the file [id].tsx.

Let’s look at an example in app/user/[id].tsx:

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

export default function UserDetail() {
  const { id } = useLocalSearchParams();

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>User ID: {id}</Text>
    </View>
  );
}

When you navigate to /user/123, the system automatically assigns id = 123. Fast and clean!

‘Battle-Tested’ Experience in Data Handling

In practice, I often work with APIs that return complex JSON strings. Printing console.log and straining your eyes to read it is truly time-consuming.

I often use tools at toolcraft.app (e.g., JSON Formatter) to clean up data. This helps me define standard TypeScript interfaces in 30 seconds, completely avoiding annoying undefined errors when working with Dynamic Routes.

Another quick tip: Prioritize using the Link component instead of the useRouter hook. Your code will look like HTML, making it extremely easy to read and maintain:

import { Link } from 'expo-router';

<Link href="/user/99" asChild>
  <Button title="View Profile" />
</Link>

Conclusion

Switching to Expo Router might feel a bit strange at first due to the new file naming convention. However, once you’re used to it, you’ll find it saves 30-40% of unnecessary boilerplate code. Try applying the structure I suggested to your next project. You’ll definitely see the difference. Happy coding!

Share: