In Expo Router, why are params always strings or string arrays, and what does a src/app/docs/[...slug].tsx route receive for /docs/guides/install?
answer
- params come from a URL
- hrefs are serialised to strings
- numbers arrive as text
- rest segment gives an array
- even one segment is an array
basics
~10 sExpo Router resolves every navigation to a URL and parses params back out of it, so values are strings; a [...slug] catch-all collects all remaining segments into a string array, giving slug ['guides', 'install'].
solid answer
~40 sEvery Expo Router navigation, whether a deep link, a `<Link>` or a `router.push` with an href object, is resolved to a **URL string** and the parameters are parsed back out of that URL. URLs only carry text, so a route parameter is a **string** (`id: '42'`, even if you passed the number 42) and Expo Router's output parameter type is a record of `string | string[]`. The array case is the **catch-all**: `src/app/docs/[...slug].tsx` matches any depth under `/docs/`, and `useLocalSearchParams()` returns `slug` as an **array of segments**, `['guides', 'install']` for `/docs/guides/install`, and still an array, `['faq']`, for a single segment. Search parameters stay individual strings. So screens convert explicitly (`Number(id)`), join or index the array, and never pass objects or dates as params; pass an id and load the data instead.
code
tsx · 15 lines// src/app/docs/[...slug].tsx
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
export default function DocPage() {
const { slug, lang } = useLocalSearchParams<{ slug: string[]; lang?: string }>();
const docPath = slug.join('/'); // '/docs/guides/install' -> 'guides/install'
return (
<View>
<Text>Doc: {docPath}</Text>
<Text>Language: {lang ?? 'default'}</Text>
</View>
);
}go deeper
Recall that Expo Router params are strings, that numbers must be converted, and that [...slug] returns an array of path segments.
Explain the reason: every navigation is serialised to a URL and parsed back, and a catch-all returns an array even for one segment.
Design URLs to carry ids and scalars, validate deep-link input, and handle missing content inside catch-alls that shadow the folder's 404.
Set the rule that navigation state must survive a URL round-trip, so every screen works from a link, a reload or a notification without in-memory hand-offs.
## Everything goes through a URL Expo Router is URL-first. However you navigate, the destination is turned into a **URL string**: - a deep link or a web address already is one; - `<Link href="/product/42">` is one; - an href object such as `{ pathname: '/product/[id]', params: { id: 42 } }` is **serialised**: the `[id]` placeholder is replaced by the encoded value and leftover params become the query string. The screen then receives parameters **parsed from that URL**. Text in, text out: that is why Expo Router's output parameter type is `Record<string, string | string[]>`. Inputs to an href may be numbers, but outputs are never numbers, booleans, objects or `undefined` for a matched route segment. ## Consequences of string-only params | You pass | The screen receives | Pitfall | |---|---|---| | `id: 42` | `'42'` | `id === 42` is always false | | `inStock: true` | `'true'` | `if (inStock)` is true even for `'false'` | | `filters: { color: 'red' }` | `'[object Object]'` | the object is lost in serialisation | | `/product/red%20mug` | `'red mug'` | decoded by the local hook | The practical rules: 1. **Convert explicitly** with `Number()`, a boolean check against `'true'`, or a schema parser. 2. **Pass identifiers, not data.** Put the product id in the URL and load the product; do not push a whole product object as a param. 3. **Validate** values that arrive from deep links, because anyone can type a URL. ## The catch-all segment A name written as **`[...name]`** is a **catch-all** (rest) segment. `src/app/docs/[...slug].tsx` matches every path under `/docs/` at any depth: - `/docs/guides/install` gives `slug: ['guides', 'install']`; - `/docs/faq` gives `slug: ['faq']`, still an **array**, even with one element; - `/docs/api/hooks?lang=ts` gives `slug: ['api', 'hooks']` and `lang: 'ts'`, because search parameters remain single strings. This fits content that mirrors a hierarchy, such as a documentation tree whose pages are fetched by path: join the array with `/` to build the lookup key. ## Ranking against other routes Catch-alls are the **least specific** matching routes: 1. Static files (`docs/changelog.tsx`) win first. 2. Single dynamic segments (`docs/[section].tsx`) come next for one-segment URLs. 3. The catch-all takes whatever is left, at any depth. 4. Only then does the not-found route render. So a catch-all in a folder effectively **replaces** that folder's 404 behaviour: every unknown `/docs/...` URL lands in it, and the screen must handle "page does not exist" itself. ## Designing URLs for the docs catch-all A catch-all is powerful but easy to overuse. For a documentation section that mirrors a content tree, a sound design looks like this: - keep **fixed pages as static files** (`docs/changelog.tsx`, `docs/index.tsx`) so they never depend on content lookups; - let `docs/[...slug].tsx` serve the **content pages**, joining `slug` into a lookup key such as `guides/install`; - render an explicit **"page not found" state** inside the catch-all when the key has no content, because the folder's `+not-found.tsx` will never be reached for those URLs; - keep **options in the query string** (`?lang=ts`), where they stay single strings and do not change which route matches. The same serialisation rule applies when linking into the catch-all: passing an array value for `slug` in an href object joins the elements with `/` in the path. The screen receives the array back, split on the slashes, which is exactly the round-trip the URL-first model promises. ## Typing it A manual generic documents the shape: `useLocalSearchParams<{ slug: string[]; lang?: string }>()`. With typed routes enabled, `useLocalSearchParams<'/docs/[...slug]'>()` derives `slug` as `string[]` from the file tree. Either way the generic is a compile-time promise; parse and validate at runtime. ## What interviewers look for - The **reason** params are strings (URL serialisation), not just the fact. - Knowing the catch-all returns an **array even for one segment**. - Knowing where catch-alls rank, and that they swallow the folder's unmatched URLs. - The design rule that follows: URLs carry ids and small scalars, never objects.
- How should a product list pass a selected product to src/app/product/[id].tsx?Pass only the id in the href and let the product screen load the product by id. Params are serialised into the URL, so objects do not survive, and an id-only URL also works as a deep link, a web address and after a reload.
- src/app/docs/ contains [...slug].tsx and +not-found.tsx; which one renders /docs/missing/page?The catch-all. It ranks ahead of the not-found route and matches any depth, so it receives `slug: ['missing', 'page']` and must render its own not-found state when no document exists for that path.
saying these in an interview costs you the question
- Passing params: { id: 42 } makes the screen receive the number 42.
- A catch-all with one segment returns a plain string.
- Search parameters on a catch-all route also become arrays.
- Objects passed as params arrive intact because navigation stays in memory.
- A catch-all route never catches URLs a +not-found file would handle.