skip to content

In Expo Router, how does a src/app/product/[id].tsx file capture a URL segment, and how does the screen read it?

level: juniorimportance: must knowfreq 60%

answer

  1. square brackets name a parameter
  2. one segment per [param]
  3. read with useLocalSearchParams
  4. the value arrives as a string
  5. static files win over [id]

basics

~10 s

Square brackets make a dynamic segment: src/app/product/[id].tsx matches /product/42 and any other single segment, and the screen reads it with useLocalSearchParams(), which returns id as the string '42'.

solid answer

~40 s

In Expo Router, a file or folder whose name is wrapped in square brackets is a **dynamic route**. `src/app/product/[id].tsx` matches `/product/42`, `/product/abc` and any other **single** segment after `/product/`, and the bracketed name becomes the parameter key. Inside the screen, `useLocalSearchParams()` from `expo-router` returns `{ id: '42' }`, always a string, together with any search parameters such as `?ref=home`; a generic like `useLocalSearchParams<{ id: string; ref?: string }>()` types it. When the route matches, `id` is never nullish. Static routes rank ahead of dynamic ones, so a `product/new.tsx` file wins for `/product/new`. When the route parameter changes, for example pushing `/product/43`, the screen instance is a new one, so code that keys data loading on `id` gets a fresh mount.

code

tsx · 15 lines
tsx
// src/app/product/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';

export default function ProductScreen() {
  const { id, ref } = useLocalSearchParams<{ id: string; ref?: string }>();
  const numericId = Number(id); // the param is always a string

  return (
    <View>
      <Text>Product {numericId}</Text>
      {ref ? <Text>Opened from {ref}</Text> : null}
    </View>
  );
}

go deeper

for a junior

Recall that [id].tsx captures one URL segment and that useLocalSearchParams returns it as a string alongside any query parameters.

for a middle

Explain matching order (static before dynamic), dynamic folders for nested paths, URI decoding, and that the generic is a type assertion only.

for a senior

Use the remount-per-param behaviour deliberately for data loading and caching, validate ids from untrusted links, and avoid reserved parameter names.

for a principal

Decide which identifiers belong in the path versus the query string, since path params become part of every shareable link the product must keep working.

## Dynamic segments in the file name Expo Router maps files under `src/app` to URLs. A name wrapped in **square brackets** turns that segment into a **route parameter** instead of a fixed word: - `src/app/product/[id].tsx` matches `/product/42`, `/product/sku-991` and `/product/abc`, but **not** `/product` or `/product/42/reviews`; a single bracket pair captures exactly **one** segment. - The text inside the brackets is the parameter **name**: `[id]` produces `id`, `[productId]` would produce `productId`. - A **folder** can be dynamic too: `src/app/product/[id]/reviews.tsx` matches `/product/42/reviews`, and every file inside that folder can read `id`. Route parameters are what make a route match. **Search parameters** (the query string, `?ref=home`) never affect matching; they ride along with any route. ## Reading the value in the screen `useLocalSearchParams()` from `expo-router` returns an object that merges **route and search parameters** for the screen it is called in: ```tsx // URL: /product/42?ref=home const { id, ref } = useLocalSearchParams<{ id: string; ref?: string }>(); // id === '42', ref === 'home' ``` Four details matter in practice: 1. **The value is a string.** URLs carry text, so `id` is `'42'`, not the number `42`; convert explicitly before arithmetic or strict comparisons. 2. **A matched route parameter is never nullish.** If the screen rendered, `id` exists. Search parameters are optional and may be `undefined`. 3. **Values are URI-decoded.** `/product/red%20mug` arrives as `'red mug'`. 4. **The generic is only a type assertion.** `useLocalSearchParams<{ id: string }>()` describes the shape to TypeScript; it does not validate or convert the value at runtime. ## Which route wins When several files could match a URL, Expo Router ranks them: | URL | Files present | Match | |---|---|---| | `/product/new` | `product/new.tsx`, `product/[id].tsx` | `new.tsx` (static beats dynamic) | | `/product/42` | `product/new.tsx`, `product/[id].tsx` | `[id].tsx` with `id: '42'` | | `/product/42/reviews` | `product/[id].tsx` only | not matched by `[id].tsx`; needs `[id]/reviews.tsx` or a catch-all | Because static routes rank first, reserving words such as `new` or `edit` as sibling files is safe; they can never be read as an id. ## Remounting and data loading When a **route parameter** changes, the screen is a different route instance. Pushing `/product/43` from `/product/42` in a stack mounts a **new** screen on top, and the first one keeps its own `id: '42'` underneath. Expo's guide states it plainly: whenever a route parameter changes, the component remounts. That is why keying data fetching on `id` is safe: each product screen owns its own data, and going back shows the previous product without refetching. ## A worked example: the shop's product routes A small shop app might lay out its product routes like this: ```text src/app/product/new.tsx -> /product/new src/app/product/[id]/index.tsx -> /product/42 src/app/product/[id]/reviews.tsx -> /product/42/reviews ``` Walking through it: - `/product/new` hits the static file, so the "create product" form can never be mistaken for a product with the id `new`. - `/product/42` hits the dynamic folder's `index.tsx`, which reads `id: '42'`. - `/product/42/reviews` hits `reviews.tsx` inside the same dynamic folder, which also reads `id: '42'` because the parameter belongs to the folder name. - `/product/42/photos` matches nothing and falls through to the not-found screen until a `photos.tsx` is added. The layout makes the URL design explicit: each resource has one canonical path, sub-pages hang off the same parameter, and reserved words live beside the parameter as static files. Interviewers often ask candidates to sketch exactly this kind of tree from a list of URLs. ## Names to avoid A few parameter names are reserved for internal use by Expo Router and React Navigation: **`screen`**, **`params`**, **`initial`** and **`state`**. A file named `[screen].tsx` collides with them, so pick a descriptive name such as `[screenId]`. The URL hash is also exposed as a special parameter named `#`. ## What interviewers probe - Can the candidate predict which file answers a URL, including the static-beats-dynamic rule? - Do they know the value is a string and that the TypeScript generic does not convert it? - Do they know `[id]` captures one segment, and that deeper paths need a nested file or a catch-all? - Do they see that each change of `id` produces a new screen instance, which shapes caching and back navigation?

  • How would /product/42/reviews be served next to src/app/product/[id].tsx?
    Move the product screen into a dynamic folder: `src/app/product/[id]/index.tsx` for `/product/42` and `src/app/product/[id]/reviews.tsx` for `/product/42/reviews`. Both files read `id` with `useLocalSearchParams()`.
  • Why might a screen named src/app/[screen].tsx behave strangely?
    `screen`, `params`, `initial` and `state` are reserved parameter names used internally by Expo Router and React Navigation. A route parameter with one of those names can collide with navigation internals, so choose a distinct name such as `[screenId]`.

A form with one blank to fill in: /product/___ accepts any single word written in the blank, but a pre-printed form for /product/new is always picked first when the word matches it exactly.

saying these in an interview costs you the question

  • useLocalSearchParams returns the number 42 for /product/42.
  • [id].tsx also matches /product/42/reviews.
  • A dynamic [id].tsx overrides a static new.tsx in the same folder.
  • The TypeScript generic on useLocalSearchParams converts values at runtime.
  • Search parameters decide which route file matches the URL.