Route Groups & Organizing app/
(auth), (tabs), and private folder conventions. Route groups keep large app/ trees navigable without polluting URLs or deep-link paths.
Search across all documentation pages
app/(auth), (tabs), and private folder conventions. Route groups keep large app/ trees navigable without polluting URLs or deep-link paths.
Quick-reference recipe card - copy-paste ready.
app/
├── _layout.tsx # Root providers + top navigator
├── index.tsx # Entry redirect hub
├── (auth)/
│ ├── _layout.tsx # Auth stack (no header)
│ ├── login.tsx # /login
│ └── register.tsx # /register
├── (app)/
│ ├── _layout.tsx # Authenticated guard shell
│ └── (tabs)/
│ ├── _layout.tsx
│ ├── index.tsx # /
│ └── settings.tsx # /settings
├── (modals)/
│ ├── _layout.tsx # presentation: modal
│ └── compose.tsx # /compose
└── _components/ # Private - NOT a route
└── ScreenChrome.tsx// app/(app)/_layout.tsx - minimal auth shell
import { Redirect, Stack } from "expo-router";
import { useSession } from "@/features/auth";
export default function AppShellLayout() {
const { session, isLoading } = useSession();
if (isLoading) return null;
if (!session) return <Redirect href="/login" />;
return <Stack screenOptions={{ headerShown: false }} />;
}When to reach for this:
presentation: "modal" options/tabs in every URL_components/ next to layoutsA production-shaped tree with auth group, app shell, tabs, modals, and private folders.
// app/_layout.tsx
import { Stack } from "expo-router";
import { AppProviders } from "@/app-providers";
export default function RootLayout() {
return (
<AppProviders>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="(auth)" />
<Stack.Screen name="(app)" />
<Stack.Screen name="(modals)" options={{ presentation: "modal" }} />
</Stack>
</AppProviders>
);
}// app/(auth)/_layout.tsx
import { Stack } from "expo-router";
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false, animation: "fade" }}>
<Stack.Screen name="login" />
<Stack.Screen name="register" />
</Stack>
);
}// app/(modals)/_layout.tsx
import { Stack } from "expo-router";
export default function ModalsLayout() {
return (
<Stack
screenOptions={{
presentation: "modal",
headerShown: true,
}}
/>
);
}// app/(app)/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
export default function TabsLayout() {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
);
}// app/_components/LoadingGate.tsx - import from layouts; never routed
import { ActivityIndicator, View } from "react-native";
export function LoadingGate() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<ActivityIndicator />
</View>
);
}URL map (groups omitted from path):
/login → (auth)/login.tsx
/register → (auth)/register.tsx
/ → (app)/(tabs)/index.tsx
/settings → (app)/(tabs)/settings.tsx
/compose → (modals)/compose.tsx| Pattern | URL impact | Purpose |
|---|---|---|
(auth)/login.tsx | /login | Group name omitted from URL |
(tabs)/index.tsx | / | Organize tab files |
[id].tsx | /:id | Dynamic segment (not a group) |
_layout.tsx | - | Navigator for folder |
_components/ | - | Private folder (underscore prefix) |
(app)/(tabs)/ is valid - both names are stripped from the URL_layout.tsx per folder for navigatorsapp/ features/
├── routing & layouts only ├── business logic
├── re-exports ├── hooks & API
├── redirects └── UI screens
└── _components/ (private)(auth) for sign-in, (app) for signed-in shell, (modals) for overlaysapp/(app)/(tabs)/orders/ maps to features/orders/index.tsx thin - redirect hub only; see Redirects & Index Routesapp/(app)/(tabs)/orders/[id].tsx is enough; deeper paths signal refactor timeapp/(tabs)/
├── _layout.tsx
├── _components/
│ └── TabIcon.tsx
├── index.tsx
└── profile.tsx_prefix marks files/folders non-routable - Expo Router ignores them for URL generation_components/ - do not put them in features/ unless reused+html.tsx, +not-found.tsx are special files - learn their roles before customizing web/not-found// app/_layout.tsx - explicit registration prevents ambiguous auto-discovery
<Stack>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(app)" />
</Stack>Explicit <Stack.Screen name="(auth)" /> documents top-level segments for reviewers and keeps screen options centralized.
Duplicate routes across groups - (auth)/index.tsx and (app)/index.tsx both map to /. Fix: Single app/index.tsx redirect hub; one group owns /.
Forgetting group parentheses - auth/login.tsx creates /auth/login, breaking marketing links. Fix: Rename to (auth)/login.tsx.
Business logic in _layout.tsx - data fetching in layouts runs on every child navigation. Fix: Auth redirect only; screens own data.
God (app) group - every screen dumped under one group. Fix: Sub-groups (tabs), (modals), feature folders.
_components without underscore - components/Header.tsx becomes /components/Header route. Fix: Prefix private folders with _.
Cross-group router.push with wrong path - pushing /(app)/settings includes group in some APIs. Fix: Use URL paths /settings or typed Href objects.
Monorepo shared app/ - white-label apps diverge in route trees. Fix: Separate apps/brand-a/app/ per binary - see ../project-setup/multiple-apps-in-one-repo/multiple-apps-in-one-repo.md.
| Alternative | Use When | Don't Use When |
|---|---|---|
Route groups (name) | Organize without URL segments | You need /auth/login in the public URL |
Flat app/ tree | <10 routes total | Parallel squads editing one folder daily |
src/routes manual React Navigation | Brownfield without file-based routing | New SDK 57 greenfield apps |
| Feature flags for sections | Seasonal tabs | Permanent auth vs app split |
| Separate Expo apps | Truly different products | White-label variants - use config instead |
| Deep folder mirroring org chart | Never recommended | - |
No. (auth)/login.tsx maps to /login, not /auth/login. Groups exist purely for filesystem organization and layout boundaries.
Yes - app/(app)/(tabs)/(hidden)/ is valid. Each group name is stripped. Avoid unnecessary nesting that obscures the route map.
Files and folders starting with _ (except _layout.tsx) are not turned into routes. Use _components/, _hooks/, etc., for colocated non-route code.
Deep links target URL paths, not group names. /settings opens (app)/(tabs)/settings.tsx regardless of group folders. Configure scheme in app.config.ts once at the app level.
Yes - one app/_layout.tsx mounts providers for both groups. Split navigators per group ((auth)/_layout, (app)/_layout), not duplicate provider trees.
Create (modals)/ with its own _layout.tsx setting presentation: "modal". Register (modals) on the root stack. URLs stay /compose, not /modals/compose.
Both would resolve to / - a conflict. Use a single app/index.tsx as the entry redirect instead.
app/ is the FSD app layer (routing only). features/, entities/, shared/ live outside app/. Route groups segment flows; feature slices segment business domains - see ../architecture-design/feature-sliced-design-for-rn/feature-sliced-design-for-rn.md.
When marketing, SEO (web), or support docs publish the path - e.g. /pricing for a public web page. Use normal folders, not groups, when the segment must appear in the URL.
Maintain a docs/routes.md table: URL → file path → feature owner. Update on every new group. Pair with typed routes so TypeScript catches drift.
app/index.tsx redirect hub(tabs) and (drawer) layout patternsapp/Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026