skip to content

In React Router, when both /teams/new and /teams/:teamId are defined, which route renders for /teams/new, and how does the router decide?

level: middleimportance: should knowfreq 48%

answer

  1. order in the array is not it
  2. every branch gets a score
  3. static beats dynamic beats splat
  4. siblings tie on definition order

basics

~20 s

/teams/new renders the static route. React Router scores every candidate branch — static segments weigh most, dynamic segments less, a splat is penalised — and picks the highest score, so declaration order only breaks ties between siblings.

solid answer

~40 s

React Router does not take the first route that matches. It flattens the route tree into branches, gives each a **score**, sorts by score, and returns the best branch that matches. In the pinned source a static segment adds 10, a dynamic `:param` segment 3, an empty segment 1, an index route gets a bonus of 2, and a `*` splat costs 2. So `/teams/new` outranks `/teams/:teamId`, which outranks `/teams/*`, whatever order you declared them in. Declaration order matters only when two **siblings** score the same — the earlier one wins. Path syntax is deliberately small: static text, `:param`, a trailing `?` for optional segments, and `*` only as the final segment after a `/`. Matching is case-insensitive unless the route sets `caseSensitive`.

code

ts · 9 lines
ts
import { createBrowserRouter } from "react-router";
import { TeamsCatchAll, TeamDetail, NewTeam } from "./teams";

// Deliberately "wrong" order: ranking still picks the most specific branch.
export const router = createBrowserRouter([
  { path: "/teams/*", Component: TeamsCatchAll },   // score 12
  { path: "/teams/:teamId", Component: TeamDetail }, // score 17
  { path: "/teams/new", Component: NewTeam },         // score 24 -> wins for /teams/new
]);

go deeper

for a junior

Know that /teams/new renders the static route and that React Router ranks routes rather than taking the first match.

for a middle

Explain the scoring order: static segments beat dynamic ones, splats are penalised, index routes get a bonus, and definition order breaks ties only between siblings.

for a senior

Diagnose misrouting from the scores: a missing or misspelled static route, duplicated sibling paths, or a splat expected to catch everything, and fix the table rather than reordering it.

for a principal

Set URL design rules for many teams so ranking stays predictable: no identical siblings, reserved static words under dynamic segments, and one owner for catch-all routes.

## The problem ranking solves A **route path** is a pattern such as `/teams/:teamId`. More than one pattern can match the same URL: `/teams/new` matches both the literal `/teams/new` and the pattern `/teams/:teamId` (with `teamId = "new"`). A router needs a rule for picking one. React Router — both `<Routes>` in declarative mode and the data router, which share the same matcher — uses **ranking** rather than first-match. ## How the matcher ranks 1. **Flatten.** The nested route tree is flattened into **branches**: one per route that can match on its own, with its full path (parent paths joined) and the chain of routes from root to leaf. 2. **Score.** Each branch path is split on `/` and scored. 3. **Sort.** Branches are sorted by score, highest first. 4. **Match.** The first branch in that sorted list whose pattern matches the URL wins. The pinned source (`computeScore`) uses these weights: - the number of segments is the starting score, - a **static** segment such as `teams` adds 10, - a **dynamic** segment such as `:teamId` adds 3, - an **empty** segment (the leading slash) adds 1, - an **index route** gets a bonus of 2, - a **splat** `*` subtracts 2 and adds nothing for itself. ## Worked example | Branch path | Score | Wins for `/teams/new`? | |---|---|---| | `/teams/new` | 3 + 1 + 10 + 10 = 24 | yes | | `/teams/:teamId` | 3 + 1 + 10 + 3 = 17 | only if the static route is absent | | `/teams/*` | 3 - 2 + 1 + 10 = 12 | only if both others are absent | Nobody expects a candidate to memorise the constants. The rule to state is **static beats dynamic beats splat**, and more specific segments beat fewer. ## When order does matter Ranking makes array order irrelevant — with one exception. When two branches have the **same score** and are **siblings**, the router keeps them in definition order, so the earlier sibling wins. Non-siblings with equal scores are left as ties. In practice that means two identical sibling paths is a bug: the second is dead. ## Path syntax you are expected to know - **Static segments** — literal text, matched case-insensitively by default; set `caseSensitive: true` on the route to change that. - **Dynamic segments** — `:name`, captured into params. - **Optional segments** — a trailing `?`, as in `:lang?/categories` or `users/:id/edit?`. Internally the router *explodes* an optional path into every variant and ranks each one. - **Splat** — `*`, only as the last segment and only after a `/`. A path like `docs*` produces a dev warning and is treated as `docs/*`. - **No regular expressions.** Patterns are this small grammar only; validate the shape of a param yourself after matching. - **Absolute child paths** — a child path starting with `/` must begin with the combined path of its parents, or the router throws. ## Why ranking instead of first match With a **first-match** rule, correctness depends on the order in which routes happen to be written, and the order is easy to break when routes live in different files or are generated. Ranking makes the route table declarative: you can add `/teams/new` anywhere and it still beats `/teams/:teamId`. The cost is that you must understand the scoring when a URL renders something unexpected — which is exactly why interviewers ask about it. The same scoring applies to `<Routes>` in declarative mode and to `createBrowserRouter`, because both call the same matching function. ## Interview traps - "Put specific routes first" is advice for first-match routers. In React Router it is harmless but irrelevant, except between identical siblings. - A splat does not "swallow" a more specific route declared after it; the splat's penalty keeps it at the bottom. - `/teams/new` rendering the team-detail page usually means the static route is missing or misspelled, not that it was declared in the wrong place. The crisp answer: "The static route renders. React Router scores branches — static over dynamic over splat — and only uses declaration order to break ties between siblings."

  • Two sibling routes both have the path 'reports'. Which one renders?
    They score identically, and for siblings React Router falls back to definition order, so the first one declared always renders and the second is unreachable. Treat it as a configuration bug; the router gives no error for it.
  • How are optional segments like ':lang?/categories' ranked?
    The router explodes an optional path into each concrete variant — here `/categories` and `/:lang/categories` — and each variant becomes its own branch with its own score. Ranking then works exactly as for ordinary paths.
  • Can a React Router path use a regular expression to restrict a param to digits?
    No. The path grammar has only static text, `:param`, optional `?` and a trailing `*`. Match broadly, then validate the param in code — for example in a loader that throws a 404 response — or restructure the URL.

A mail sorter with labelled bins sends each letter to the most specific bin that fits: a named building beats 'any building on this street', which beats 'anything else in town', no matter which bin was installed first. Only two bins with identical labels are settled by which one was put up first.

saying these in an interview costs you the question

  • The first route declared that matches the URL always wins
  • A splat route declared before a static route will swallow it
  • Path matching is case-sensitive by default
  • You can put regular expressions in a route path to constrain params
  • A star can appear anywhere in a path, like docs*