Expo Router Advanced Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Nest one navigator per _layout.tsx: Each folder layout exports exactly one Stack, Tabs, NativeTabs, SplitView, or headless Slot/Navigator - mixing two navigators in one layout file creates ambiguous history.
Give every tab its own stack when pushing detail: Put home/_layout.tsx → <Stack /> under each tab that needs list → detail; flat tab folders lose independent back stacks.
Navigate with absolute hrefs across tab boundaries: Prefer router.push('/inbox/t2') over ../ relative paths when crossing tabs - relative segments resolve against the current navigator context.
Anchor modal routes for deep links: Export unstable_settings = { anchor: '(tabs)' } on the stack that owns modals so cold-start /compose does not wipe the tab shell.
Use router.replace after auth transitions: Login, logout, and role changes should replace history - push lets users swipe back into credential or stale-role screens.
Hydrate session before rendering Stack.Protected: Keep splash visible until SecureStore / token read completes - rendering guards while isLoading causes redirect flicker and loops.
Single authority for auth gates: Root Stack.Protected groups beat sprinkling <Redirect /> in every nested layout - competing redirects cause infinite bounce between / and /sign-in.
Never duplicate screens across Protected groups: A route file may appear in only one Stack.Protected declaration - nest guards for roles instead of copying profile into two groups.
Treat Protected as UX, not security: Client guards do not remove routes from the bundle - validate every API call server-side; see ../auth-session/.
Choose Expo Router modals over RN <Modal> for linkable flows: presentation: 'modal' routes participate in deep links and Android back - reserve RN Modal for ephemeral alerts with no URL.
Render form sheet chrome in content on Android: Native stack headers and nested stacks inside formSheet do not render on Android - title and actions belong in the sheet body.
Cap form sheet detents at three on Android: sheetAllowedDetents with more than three values is truncated - design cross-platform sheets with ≤3 snap points.
Declare every native tab with NativeTabs.Trigger: Unlike JavaScript Tabs, files are not auto-added to the bar - missing triggers mean routes exist but are unreachable from the tab UI.
Do not use disabled on native tabs for authorization: disabled blocks tab bar taps only - router.push still navigates; use Stack.Protected or hidden for true removal.
Resolve feature-flag tab visibility before first mount: Toggling hidden on NativeTabs.Trigger at runtime remounts the navigator and clears stacks - decide tab sets at boot.
Drive tablet master-detail from URL segments: Selection state belongs in [id].tsx routes, not useState(selectedId) - deep links and rotation restore correctly.
Ship responsive split fallback for Android tablets: SplitView is iOS-native alpha - use useWindowDimensions + row layout and per-section Stack on Android and narrow widths.
Show an empty detail placeholder on tablet: /orders without an id should render "Select an order" in the secondary pane - mail-client parity prevents confusing blank columns.
Limit yourself to one SplitView per app: Nesting split views or placing them inside tabs throws - split belongs at root with sidebar sections replacing bottom tabs on iPad.
Start RSC with Server Functions only in SDK 57: Enable reactServerFunctions inside normal client routes - avoid reactServerComponentRoutes in production until Stack/Tabs support returns.
Wrap streamed Server Function UI in Suspense: Every renderX() call from "use server" needs <React.Suspense fallback={…}> - otherwise loading states block the whole screen.
Reach for <Slot /> before custom Navigator: Headless chrome (headers, onboarding progress) rarely needs useNavigationBuilder - custom navigators increase maintenance and SDK import surface.
Import navigation primitives from expo-router paths: SDK 56+ routes @react-navigation/* through expo-router and expo-router/react-navigation - direct package imports break on upgrade.
Pass redirect query param through sign-in: Deep link to protected content should resume post-login via router.replace(redirect ?? '/(tabs)') - do not drop the intended destination.
Test cold-start deep links in CI matrices: Verify myapp://inbox/t2, modal anchors, logged-out redirects, and Android hardware back on each release - navigation regressions are production incidents.
useSegments()).Stack.Protected / Tabs.Protected at root.app/index.tsx only for default landing path.Tabs when you need Tabs.Protected inside the tab layout or fully custom tab bar JSX.isLoading gate before any navigator.Stack.Protected authority.router.replace after login.index.tsx in competing groups.renderRouter for href navigation and Protected redirects.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 19, 2026