skip to content

In React Router v7, which URLs do the route paths ":lang?/categories" and "files/*" match, and where may the * appear?

level: middleimportance: should knowfreq 44%

answer

  1. question mark after a segment
  2. zero or one of that segment
  3. star swallows the rest, slashes included
  4. only at the end, after a slash

basics

~10 s

:lang?/categories matches /categories and /en/categories, with lang undefined when absent. files/* matches /files and anything below it, like /files/a/b. A * only works as the final segment, directly after a slash.

solid answer

~40 s

A `?` after a segment makes it optional. `:lang?/categories` matches `/categories` and `/en/categories` but not `/en/fr/categories`; the `lang` param is `undefined` when the segment is missing. Static segments can be optional too: `users/:userId/edit?` matches with or without `/edit`. A splat `*` matches everything after its slash, including further slashes: `files/*` matches `/files`, `/files/readme` and `/files/a/b/c`, and the router exposes the remainder under the `*` param (an empty string for bare `/files`). The `*` must be the last segment and follow a `/`: `files*` is treated as `files/*` with a console warning, and a `*` in the middle of a path is not a wildcard at all. Splats rank below static and dynamic routes, which is why a top-level `*` works as a not-found route.

code

tsx · 12 lines
tsx
import { createBrowserRouter } from "react-router";

export const router = createBrowserRouter([
  // /categories and /en/categories
  { path: ":lang?/categories", element: <Categories /> },
  // /users/7 and /users/7/edit share one component
  { path: "users/:userId/edit?", element: <UserProfile /> },
  // /files, /files/readme, /files/a/b/c
  { path: "files/*", element: <FileBrowser /> },
  // anything nothing else matched
  { path: "*", element: <NotFound /> },
]);

go deeper

for a junior

Recall that ? makes a segment optional, that * matches the rest of the URL, and that a top-level * route is the usual not-found page.

for a middle

Explain the splat's placement rule and value, optional static versus dynamic segments, and that a missing optional param is undefined.

for a senior

Judge when optional segments create ambiguous or unvalidated URLs, and when a splat with descendant routes is the cleaner boundary for a feature.

for a principal

Decide how far URL flexibility should go: optional prefixes and splats ease migrations, but each widens the set of URLs the app must validate.

## Three segment types beyond the basics React Router v7 path patterns are built from segments separated by `/`. Beyond **static** segments (`files`) and **dynamic** segments (`:fileId`), two forms handle the edges of a URL scheme: - **optional segments**, written with a trailing `?`; - the **splat**, written `*`, which matches the rest of the URL. ## Optional segments A `?` after a segment means "zero or one of this segment". It works on both kinds: | Pattern | Matches | Does not match | |---|---|---| | `:lang?/categories` | `/categories`, `/en/categories` | `/en/fr/categories` | | `users/:userId/edit?` | `/users/7`, `/users/7/edit` | `/users/7/view` | An optional dynamic segment that is absent is reported as `undefined` in the params, so code must handle both cases. Internally, the router expands a pattern with optional segments into concrete alternatives (with and without the optional parts, minus ambiguous lower-priority combinations) and ranks those like ordinary routes, so an optional pattern behaves as a bundle of alternatives rather than a special matcher. Typical uses: 1. a locale prefix that is omitted for the default language (`:lang?/pricing`); 2. a sub-mode of one screen that shares a component, such as view and edit on `users/:userId/edit?`; 3. keeping legacy URLs working while a segment is being introduced. Optional segments are convenient but can hide ambiguity. `:lang?/:page` matches `/about` with `page` set to `about`, and also `/en/about`; it will also accept `/xx/about` with any two letters. Validate the optional value rather than assuming it is well-formed. ## Splats A splat matches **everything after its slash**, including further slashes: - `files/*` matches `/files`, `/files/readme` and `/files/a/b/c`; - the matched remainder is exposed as the `*` param: `a/b/c`, without a leading slash, and an empty string at bare `/files`; - a top-level `path: "*"` matches any URL and, because splats rank lowest, only wins when nothing more specific does, which makes it the standard not-found route. Rules for where `*` may appear: - It must be the **last** segment and must follow a `/`. `files*` still works but logs a warning that it will be treated as `files/*`. - A `*` elsewhere in the pattern (`files/*/edit`) is **not** a wildcard; the router escapes it and matches it as a literal `*` character, so the route effectively never matches real URLs. ## Splats and descendant routes In declarative mode, a component rendered by a splat route can render its own `<Routes>`, which match against the **remaining** part of the URL. That is how a self-contained feature can own its internal routing: ```tsx <Route path="files/*" element={<FilesApp />} /> // inside FilesApp: <Routes> <Route index element={<FileList />} /> <Route path=":fileId" element={<FileView />} /> </Routes> ``` If the parent route lacks the trailing `/*`, React Router warns that deeper URLs will stop matching the parent, so the descendant routes will never render. ## Choosing between them | Need | Use | |---|---| | a segment that may be missing | optional segment `?` | | an arbitrary-depth path, such as a file tree | splat `*` | | a not-found page | top-level `*` route | | a fixed number of values | separate dynamic segments | ## Common mistakes - **Treating a missing optional param as an empty string.** It is `undefined`; code like `params.lang.toUpperCase()` throws on `/categories`. - **Putting `*` in the middle of a path** to mean "one segment". Use a dynamic segment such as `:section` instead; a mid-path `*` is literal text. - **Using a splat where a fixed depth is known.** `projects/*` for `projects/:projectId/tasks/:taskId` hides the structure from ranking and from the params, and forces the component to parse the remainder by hand. - **Forgetting the trailing `/*` on a route that renders descendant `<Routes>`.** The parent stops matching as soon as the URL goes deeper, and React Router warns about it. - **Chaining several optional segments.** Each one multiplies the URL shapes the route accepts, so ambiguity and validation work grow quickly. ## Summary - `?` makes one segment optional; missing optional params are `undefined`. - `*` matches the rest of the URL, must be last and follow a slash. - The splat value is available under the `*` param, empty at the bare prefix. - Splats rank lowest, so they make natural fallbacks.

  • In React Router v7, does "files/*" still match the bare URL /files?
    Yes. The splat may match nothing, so `/files` and `/files/` both match, with the `*` param set to an empty string. If `/files` should show a different screen, add an index route or a separate `files` route; the static route will outrank the splat.
  • In React Router v7, why can ":lang?/:slug" be risky for a marketing site?
    Because both segments are loosely typed, `/about` matches with `slug` set to `about`, and `/de/about` with `lang` set to `de`, but `/anything/about` also matches, treating `anything` as a language. Validate the optional value against the supported locales and redirect or show not-found otherwise.

saying these in an interview costs you the question

  • A splat can sit in the middle of a path to match one segment
  • files/* requires at least one segment after /files
  • An absent optional segment is reported as an empty string
  • Optional segments only work for dynamic segments, not static ones
  • A top-level * route must be declared last to act as a fallback