Link & href
Declarative navigation and prefetch-friendly links. Link maps to anchor-like navigation on web and accessible press targets on native - router handles imperative flows after side effects.
Search across all documentation pages
Declarative navigation and prefetch-friendly links. Link maps to anchor-like navigation on web and accessible press targets on native - router handles imperative flows after side effects.
Quick-reference recipe card - copy-paste ready.
import { Link, router, type Href } from "expo-router";
import { Pressable, Text, View } from "react-native";
// String href - static paths only
<Link href="/settings">Settings</Link>
// Object href - dynamic segments (preferred)
<Link href={{ pathname: "/orders/[id]", params: { id: "42" } }}>
Order 42
</Link>
// Imperative after mutation
router.replace("/(tabs)");
// Prefetch on hover/focus (web) or manually
<Link href="/orders" prefetch />// src/navigation/routes.ts
import type { Href } from "expo-router";
export const routes = {
home: "/" as Href,
settings: "/settings" as Href,
orderDetail: (id: string): Href => ({
pathname: "/orders/[id]",
params: { id },
}),
};When to reach for this:
Link - list rows, inline text links, tab-adjacent navigation, SEO-friendly web anchorsrouter.push - navigate after form submit, scan QR, or timer callbackrouter.replace - post-login, post-logout, onboarding completionprefetch - screens with heavy bundles or slow data waterfallsList screen with Link, custom Pressable via asChild, imperative navigation, and centralized href builders.
// src/navigation/routes.ts
import type { Href } from "expo-router";
export function orderDetailHref(id: string): Href {
return { pathname: "/orders/[id]", params: { id } };
}
export function settingsHref(): Href {
return "/settings";
}// features/orders/ui/OrdersListScreen.tsx
import { Link, router } from "expo-router";
import { Pressable, Text, View } from "react-native";
import { orderDetailHref } from "@/navigation/routes";
const ORDERS = [
{ id: "101", label: "Widget shipment" },
{ id: "102", label: "Replacement part" },
];
export function OrdersListScreen() {
return (
<View style={{ flex: 1, padding: 16, gap: 12 }}>
{ORDERS.map((order) => (
<Link key={order.id} href={orderDetailHref(order.id)} prefetch asChild>
<Pressable style={{ padding: 12, backgroundColor: "#f1f5f9", borderRadius: 8 }}>
<Text>{order.label}</Text>
</Pressable>
</Link>
))}
<Link href="/settings">Account settings</Link>
<Pressable
onPress={() => router.push(orderDetailHref("draft"))}
accessibilityRole="button"
>
<Text>Create draft (imperative)</Text>
</Pressable>
</View>
);
}// After successful form submit - replace avoids back stack to form
import { router } from "expo-router";
async function onSubmit() {
await saveProfile();
router.replace("/(tabs)/profile");
}// Dismiss modal flow
import { router } from "expo-router";
function closeModal() {
if (router.canGoBack()) {
router.back();
} else {
router.replace("/");
}
}href shapes| Form | Example | Typed routes |
|---|---|---|
| String path | "/settings" | Static routes |
| Object | { pathname: "/orders/[id]", params: { id } } | Dynamic routes |
| Relative | "./details" | Sibling within folder |
| External | "https://expo.dev" | Opens browser / external app |
// Relative navigation within app/orders/
<Link href="./[id]" params={{ id: "5" }}>Relative</Link>href="/search?q=shoes" or params: { q: "shoes" } depending on route shapepathname values fail at compile time - see Typed RoutesLink vs router| API | Style | Best for |
|---|---|---|
<Link href> | Declarative | Lists, inline navigation, prefetch |
router.push | Imperative | Post-async, programmatic redirects |
router.replace | Imperative | Auth transitions, no back |
router.back | Imperative | Cancel, modal dismiss |
router.navigate | Imperative | Go to route if exists, else push (web-like) |
// Link props that matter in production
<Link
href="/orders"
prefetch // preload route module
replace // replace history instead of push
push // force push even when replace might default
asChild // merge into child Pressable
disabled={!isReady}
/>prefetch on Link - Expo Router loads the target route's JavaScript bundle ahead of navigationasChild pattern<Link href="/settings" asChild>
<Pressable style={styles.row}>
<Text>Settings</Text>
</Pressable>
</Link>onPress, href on web) - use Pressable or custom components that spread propsLinkaccessibilityRole="link" on web; Pressable inherits link behavior from Link// packages/navigation/src/hrefs.ts (monorepo)
export const orderDetailHref = (id: string) =>
({ pathname: "/orders/[id]", params: { id } }) as const;packages/navigation when route shapes matchString concatenation for dynamic paths - href={'/orders/' + id} bypasses typed routes and breaks when segments change. Fix: Object form or orderDetailHref(id).
router.push after login - back gesture returns to login. Fix: router.replace.
Link wrapping non-forwarding components - custom rows ignore press. Fix: asChild + spread props, or use Pressable.
External URLs in router.push - may not open browser. Fix: import { openURL } from "expo-linking" for https:// links.
Prefetching protected routes - flashes unauthorized content. Fix: Gate prefetch behind session check or skip on auth screens.
Missing accessibilityRole on imperative Pressable - screen readers announce "button" for navigation. Fix: accessibilityRole="link" or prefer Link.
Huge param payloads in href - navigation state bloat, logs leak data. Fix: Pass IDs only; load bodies from store or API.
| Alternative | Use When | Don't Use When |
|---|---|---|
<Link href> | Default list/inline navigation | After async mutation before navigate |
router.push / replace | Imperative control | You need prefetch and link semantics |
useNavigation().navigate | React Navigation escape hatch | Standard Expo Router paths exist |
Redirect component | Declarative auth gate in layout | Button-triggered navigation |
| Deep link URL manually | Push notifications payload | In-app taps - use typed href |
href string | Quick prototypes | Production dynamic routes |
Link for user-tappable UI that navigates to a known route.router.push after async work completes (save, scan, permission grant).router.replace when back should not return to the current screen.router.push({ pathname: "/search", params: { q: "shoes", sort: "price" } });Read with useLocalSearchParams(). Prefer path segments for entity IDs; query for filters.
Yes - Link renders an anchor on web with client-side navigation. String href values improve crawlability for public web builds.
<Link href="/checkout" disabled={!cartReady} asChild>
<Pressable disabled={!cartReady}>
<Text>Checkout</Text>
</Pressable>
</Link>Set disabled on both Link and child when using asChild.
Returns whether the stack has a prior screen. Use before router.back() in modals opened via deep link - fall back to router.replace("/").
import * as Linking from "expo-linking";
await Linking.openURL("https://docs.expo.dev/router/introduction/");Do not pass external URLs to router.push unless you've configured external linking handlers.
Prefer <Link prefetch />. For advanced cases, consult Expo Router SDK 57 docs for prefetch utilities on the router instance - availability evolves per minor release.
./[id] resolves relative to the current route's directory - useful inside app/orders/ siblings. Prefer absolute builders in shared modules for clarity.
navigate deduplicates if the route is already focused (web-like). push always adds a stack entry. Use push for intentional duplicate detail stacks; navigate for tab-like behavior.
Return type Href from expo-router. With experiments.typedRoutes, invalid pathnames error at compile time. See Typed Routes.
Href safetyRedirect vs router.replaceLink vs imperative rulesStack 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