skip to content

In Expo Router, when do you use <Link href> instead of calling router.push, and what does Link's asChild prop change?

level: juniorimportance: must knowfreq 55%

answer

  1. declarative tap versus code after logic
  2. href: string or { pathname, params }
  3. Link renders Text by default
  4. asChild forwards onPress to one child
  5. external URLs go to the OS

basics

~20 s

Link navigates declaratively when a tap should reach a known href and renders a real anchor on web; the router object suits navigation after logic, such as a finished request. asChild hands Link's press to one child, like a Pressable.

solid answer

~40 s

`<Link href>` from `expo-router` is the declarative way to navigate: the `href` is a path string (`'/medicine/42'`) or an object with `pathname` and `params`, and on web it renders a real anchor with that URL. By default Link renders its children inside a `Text`, which is fine for text links but lays out `View` children badly; `asChild` makes Link forward its props (`onPress`, `href`, `role`) to its **single child**, so a `Pressable` card becomes the link. Use `router` (the `router` export or `useRouter()`) when navigation must happen **after logic**: once a form submits, a request succeeds, or a condition is checked. Both take the same `Href`, both go through the same routing, and an absolute URL such as an `https:` or `tel:` link is handed to the operating system instead of the router.

code

tsx · 24 lines
tsx
// src/components/medicine-card.tsx
import { Link, router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';

export function MedicineCard({ id, name }: { id: string; name: string }) {
  return (
    <View>
      <Link href={{ pathname: '/medicine/[id]', params: { id, from: 'search' } }} asChild>
        <Pressable>
          <Text>{name}</Text>
        </Pressable>
      </Link>
      <Pressable
        onPress={async () => {
          await addToCart(id);
          router.push('/cart'); // navigate only after the request succeeds
        }}>
        <Text>Add to cart</Text>
      </Pressable>
    </View>
  );
}

declare function addToCart(id: string): Promise<void>;

go deeper

for a junior

Recall that Link navigates declaratively with an href string or { pathname, params } object, and that asChild hands the press to a single Pressable child.

for a middle

Explain how href objects resolve to paths and query strings, why Link's default Text wrapper breaks View children, and when logic calls for the router object.

for a senior

Keep navigation declarative where possible for web anchors and review clarity, and treat hrefs from data as untrusted because absolute URLs leave the app.

for a principal

Set conventions for where navigation decisions live, balancing declarative links against imperative flows so navigation stays discoverable in a large codebase.

## Two ways to trigger the same navigation Expo Router gives you one routing engine and two ways to call it: - **`<Link href>`**: a component. The user taps it and navigation happens. It is declarative, visible in the component tree, and on web it renders an actual anchor element with a real URL, so middle-click, "open in new tab" and crawlers work. - **The `router` object**: functions such as `router.push`, `router.navigate` and `router.replace`, available as the `router` export from `expo-router` or through the `useRouter()` hook. It is imperative: you call it from code. Both accept the same **`Href`** type and end up in the same routing queue, so there is no difference in what screen appears; the choice is about **where the decision is made**. ## The href: string or object An `href` can be written two ways: 1. A **path string**: `'/medicine/42'`, `'/cart'`, `'/search?q=ibuprofen'`. 2. An **object** with `pathname` and optional `params`: `{ pathname: '/medicine/[id]', params: { id: 42, from: 'search' } }`. In the object form, a param whose name matches a bracket in the pathname fills that segment, and every other param becomes a **query parameter**, so the example resolves to `/medicine/42?from=search`. The object form avoids hand-building strings and escaping values, and typed routes can check it. ## Link's rendering and asChild By default Link renders its children **inside a `Text`**. That suits inline text links, but a `View`-based card inside a `Text` has surprising layout. The **`asChild`** prop changes this: - Link renders **no wrapper** of its own; - it forwards its props, including `onPress`, `href` and `role`, to its **single child**; - the child must accept `onPress` (or `onClick` on web), which `Pressable` does. Passing more than one child, or a child that cannot receive the press handler, is an error: Link reports that with `asChild` you must pass a single child element that emits `onPress`. | Need | Use | |---|---| | Inline text link | `<Link href="/cart">View cart</Link>` | | Tappable card or button | `<Link href="/medicine/42" asChild><Pressable>...</Pressable></Link>` | | Navigate after a request or check | `router.push(...)` / `router.replace(...)` in a handler | | Navigate on screen focus with no UI | `<Redirect href>` or `router.replace` in a focus effect | ## When the router object is the right tool - **After async work**: submit a prescription upload, await the response, then navigate to the confirmation. - **Conditional destinations**: route to `/cart` or `/sign-in` depending on state known only at press time. - **Non-UI triggers**: a notification handler or a timer, where there is nothing to render. - **Stack control**: `back`, `dismiss`, `dismissTo` and `dismissAll` have no Link equivalent except the `dismissTo` prop. Prefer Link for anything that is simply "tap goes to URL": it is easier to read, works as a real link on web, and supports extras such as `prefetch`, which renders the target screen off-screen ahead of time. ## External and special URLs If an `href` starts with a well-known scheme such as `https:`, `mailto:` or `tel:`, with any scheme followed by `//`, or with a bare `//`, Expo Router does **not** route it. It hands the URL to the operating system through React Native's `Linking`, so `<Link href="tel:+15550100">Call pharmacy</Link>` opens the dialler. This matters when an href comes from data: an unexpected absolute URL leaves the app instead of failing. ## Link on web versus native The same `<Link>` behaves appropriately per platform: - On **web** it renders an anchor whose `href` attribute is the resolved URL with route-group segments stripped, and it has the `link` role. A plain left click is routed in-app, while clicks with modifier keys, or with a `target` such as `_blank`, are left to the browser, so "open in new tab" works as users expect. - On **native** it renders a pressable `Text` (or your `asChild` child) with the `link` role, which screen readers announce as a link. - The **`prefetch`** prop asks Expo Router to render the target screen off-screen when the Link renders on a focused screen, so the first tap on a heavy screen, such as a medicine detail page with images, feels instant. A prefetched screen cannot use the imperative router or change its options until it is actually navigated to. These are reasons to keep ordinary navigation declarative: the router object gives none of them for free. ## Common mistakes - Wrapping a `Pressable` in Link **without** `asChild`, then fighting nested-press and layout issues. - Building `'/medicine/' + name` by hand, forgetting to encode spaces; the object form encodes values. - Calling `router.push` during render instead of in a handler or effect. - Using the router object for every tap, losing web anchors and making navigation harder to spot in review.

  • What URL does { pathname: '/medicine/[id]', params: { id: '42', from: 'search' } } resolve to in Expo Router?
    `/medicine/42?from=search`. The `id` param fills the matching bracket segment and is URL-encoded; any param that does not match a bracket, here `from`, is appended as a query parameter.
  • What happens if a Link's href is 'https://example.com/leaflet.pdf'?
    Expo Router treats an href with a scheme as external and hands it to the operating system through `Linking`, so the browser or another app opens it. It never becomes an in-app route, which is why hrefs built from untrusted data need checking.

Link is a signpost fixed to a door: you walk through when you choose. The router object is a receptionist who sends you on only after checking your paperwork.

saying these in an interview costs you the question

  • Link and router.push lead to different routing engines.
  • asChild lets Link wrap several sibling children at once.
  • Link renders its children inside a View by default.
  • Params in an href object must all match bracket segments.
  • An https: href opens inside the app as a route.