RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-03-13Intermediate

Navigation in Rork Apps Without the Rebuild — An Expo Router Blueprint

Understand the Expo Router file structure Rork generates, then layer tabs, stacks, modals, auth guards, and deep links without rework — including the mistakes that cost me half a day.

Expo Router5NavigationReact Native209Rork515Deep Linking6Tab BarStack Navigation

Navigation in Rork Apps Without the Rebuild — An Expo Router Blueprint

As an indie developer building apps with Rork, I've learned that navigation is usually the first thing to break when an app grows. One of my own projects started fine with four tabs, but once I added detail screens and a settings modal, the transitions tangled and I ended up rebuilding the whole navigation layer. The root cause was simple: I hadn't mapped which file owned which screen before reading the generated code.

Apps built with Rork use Expo Router — a file-system-based routing library that works similarly to Next.js for the web. Understanding it up front makes your Rork prompts far more precise and saves you from the kind of costly rebuild I just described. This guide walks through tabs, stacks, and modals, then auth guards and deep linking, in the order you'd actually implement them.

This guide walks through tab, stack, modal, and authenticated navigation patterns, plus deep linking — all in the context of Rork.

What Is Expo Router?

Expo Router maps your app/ directory structure directly to routes and navigation. Each file becomes a screen; each folder can become a navigation group or layout.

Rork sets up Expo Router automatically, but knowing the file structure helps you write better prompts and understand what the AI is generating on your behalf.

Basic Directory Structure

app/
├── _layout.tsx          ← Root layout (shared config)
├── index.tsx            ← Home screen (/)
├── (tabs)/              ← Tab navigation group
│   ├── _layout.tsx      ← Tab definitions
│   ├── home.tsx         ← Home tab
│   ├── explore.tsx      ← Explore tab
│   └── profile.tsx      ← Profile tab
├── (auth)/              ← Auth group
│   ├── login.tsx
│   └── signup.tsx
└── modal.tsx            ← Full-screen modal

Folders wrapped in parentheses () are route groups — they organize layouts without affecting the URL path. This is the one rule worth internalizing before you write a single router.push. The real path for (tabs)/home/[id].tsx is /home/123, not /tabs/home/123. I spent half a day chasing a "nothing happens on tap" bug that turned out to be router.push('/tabs/home/1'). Groups are the only place where the directory name and the URL disagree.


Tab Navigation

Bottom tab bars are the most common navigation pattern in mobile apps. Tell Rork what tabs you need and it will generate the correct Expo Router structure.

Example Rork prompt:

Create an app with four bottom tabs: Home, Search,
Favorites, and Profile. Each tab should have an
Ionicons icon and a label.

A typical generated (tabs)/_layout.tsx:

import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
 
export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#007AFF',
        tabBarInactiveTintColor: '#8E8E93',
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="favorites"
        options={{
          title: 'Favorites',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="heart" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Stack Navigation (Push / Pop)

For detail screens, forms, or any flow where users navigate forward and back, use stack navigation nested inside a tab.

Nested Stack Inside a Tab

app/
└── (tabs)/
    ├── _layout.tsx
    └── home/
        ├── index.tsx        ← List screen (/home)
        └── [id].tsx         ← Detail screen (/home/123)

Dynamic routes ([id].tsx) accept URL parameters:

// app/(tabs)/home/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
 
export default function DetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
 
  return (
    <View style={{ flex: 1, padding: 16 }}>
      <Text>Item ID: {id}</Text>
    </View>
  );
}

Navigating to the Detail Screen

import { router } from 'expo-router';
 
<TouchableOpacity onPress={() => router.push(`/home/${item.id}`)}>
  <Text>{item.title}</Text>
</TouchableOpacity>

Example Rork prompt:

Build a news app with a list screen and a detail screen.
Tapping a news item navigates to the detail screen using the
article ID as a URL parameter. A back button returns to the list.

Modal Screens

Use modals for settings, confirmations, or any screen that should slide up over the current context.

// app/_layout.tsx (root layout)
import { Stack } from 'expo-router';
 
export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen
        name="modal"
        options={{
          presentation: 'modal',
          title: 'Settings',
        }}
      />
    </Stack>
  );
}

Opening the modal from anywhere in the app:

import { router } from 'expo-router';
 
<TouchableOpacity onPress={() => router.push('/modal')}>
  <Text>Open Settings</Text>
</TouchableOpacity>

Authentication Guard

Redirecting unauthenticated users is a pattern almost every app needs. Here's how to implement it cleanly with Expo Router:

// app/_layout.tsx
import { useEffect } from 'react';
import { useRouter, useSegments, Slot } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
 
export default function RootLayout() {
  const { user, loading } = useAuth();
  const segments = useSegments();
  const router = useRouter();
 
  useEffect(() => {
    if (loading) return;
 
    const inAuthGroup = segments[0] === '(auth)';
 
    if (!user && !inAuthGroup) {
      // Not signed in — redirect to login
      router.replace('/(auth)/login');
    } else if (user && inAuthGroup) {
      // Already signed in — go to the app
      router.replace('/(tabs)/home');
    }
  }, [user, loading, segments]);
 
  return <Slot />;
}

Example Rork prompt:

Build an app where unauthenticated users are redirected to
the login screen. After signing in, redirect to the home tab.
Manage auth state with Zustand.

Deep Linking

Deep links let external sources — push notifications, websites, other apps — open a specific screen in your app.

Configure app.json

{
  "expo": {
    "scheme": "myapp",
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "myapp.com",
              "pathPrefix": "/item"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Expo Router automatically handles deep link resolution from this config. For example, opening myapp://home/123 routes to app/(tabs)/home/[id].tsx with id = "123".

💡
To configure deep linking with Rork, prompt it with: "Set up deep linking with the scheme myapp, and handle the /item/:id path to open the detail screen." Rork will update both app.json and the relevant route files.

When Navigation Fires Before the Layout Mounts

The bug that usually shows up right after you add an auth guard is Attempted to navigate before mounting the Root Layout component. Your useEffect calls router.replace() while the root navigator hasn't finished mounting.

In my case the auth state loaded from AsyncStorage faster than the first render, so the redirect ran too early. The fix has two gates:

// app/_layout.tsx
import { useEffect } from 'react';
import { Slot, useRouter, useSegments, useRootNavigationState } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useAuth } from '@/hooks/useAuth';
 
SplashScreen.preventAutoHideAsync();
 
export default function RootLayout() {
  const { user, loading } = useAuth();
  const segments = useSegments();
  const router = useRouter();
  const navState = useRootNavigationState();
 
  useEffect(() => {
    // 1. Wait until the navigator has actually mounted
    if (!navState?.key) return;
    // 2. Wait until auth state has been restored
    if (loading) return;
 
    const inAuthGroup = segments[0] === '(auth)';
 
    if (!user && !inAuthGroup) {
      router.replace('/(auth)/login');
    } else if (user && inAuthGroup) {
      router.replace('/(tabs)/home');
    }
 
    SplashScreen.hideAsync();
  }, [navState?.key, user, loading, segments]);
 
  return <Slot />;
}

useRootNavigationState() only returns a key once the navigator is live. Gate your redirect on it and the launch crash disappears. Hiding the splash screen manually in the same effect also removes the brief flash of the login screen on cold start.

One structural note. Where the auth decision itself lives is worth thinking through alongside implementing user authentication in Rork with Firebase and Supabase. Keeping the navigation layer as a thin consumer of that decision is what makes swapping the auth backend later a small change rather than a rewrite.


Put the Not-Found Screen In Early

Once deep linking is on, someone will eventually open your app with a stale or mistyped URL. Expo Router reserves the filename +not-found.tsx for exactly this case.

// app/+not-found.tsx
import { Link, Stack } from 'expo-router';
import { View, Text } from 'react-native';
 
export default function NotFoundScreen() {
  return (
    <>
      <Stack.Screen options={{ title: "We couldn't find that screen" }} />
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }}>
        <Text style={{ marginBottom: 16 }}>
          This link may be out of date, or the screen has moved.
        </Text>
        <Link href="/(tabs)/home" replace>
          <Text style={{ color: '#007AFF' }}>Back to home</Text>
        </Link>
      </View>
    </>
  );
}

The replace prop matters. Without it, the not-found screen stays in the history stack, and a back gesture from home drops the user straight back into the error state. It's a small thing that feels wrong immediately on a real device.

You don't need a fresh build to test deep links, either:

# iOS simulator
xcrun simctl openurl booted "myapp://home/123"
 
# Android emulator
adb shell am start -W -a android.intent.action.VIEW -d "myapp://home/123"

Verify two things before you submit: a bad ID lands on +not-found, and a valid ID opens the detail screen. That's one fewer review rejection.


Navigation Best Practices with Rork

These five principles will improve both your prompts and the quality of generated navigation code.

1. Define your navigation structure before prompting

Map out which screens belong in which tab, which screens are modals, and what the auth flow looks like before writing a single prompt. A clear mental model leads to cleaner generated code.

2. Be explicit about transitions in your prompts

Phrases like "navigate to the detail screen," "replace the login screen with home after sign-in," and "show this as a modal" directly translate into the right Expo Router calls.

Phrases like "navigate to the detail screen," "replace the login screen with home after sign-in," and "show this as a modal" directly translate into the right Expo Router calls. For prompt granularity itself, how to write better prompts for Rork covers the tactics I lean on.

Keep (auth)/ for unauthenticated flows, (tabs)/ for the main app, and (admin)/ for any internal tooling. This structure is clean, scalable, and what Rork naturally generates when prompted clearly.

4. Know when to use push vs. replace

  • router.push() → adds to history (user can go back)
  • router.replace() → replaces history entry (login redirects, onboarding completion)
  • router.back() → goes to the previous screen

5. Centralize header styles in _layout.tsx

Rather than styling headers per-screen, use screenOptions in _layout.tsx to keep consistent header appearance across an entire stack or tab group.


Where to Go Next

The habit that finally spared me the rebuild I mentioned at the start was sketching the directory structure on paper before adding a screen. Groups don't appear in the URL. Redirects wait for mount. The not-found screen goes in early. Internalize those three and most of the rework disappears.

If you want something concrete to do today: open app/, trace how your groups nest, and add a single +not-found.tsx. That alone gets you ready to take deep linking seriously.

Start with tabs, then layer stacks and modals as the app asks for them. That's the order I've settled into, and it has held up across several projects.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-07-15
A Yellow Warning in Dev, a Crash on Resume — Functions in Route Params
Rork generated navigation code that put a function and a Date into route params. In development it was only a warning. After the OS reclaimed the process, resuming the app crashed with params.onFavorite is not a function. Here is the cause and the fix.
Dev Tools2026-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →