In React Router v7, sibling routes "projects/new" and "projects/:projectId" both match /projects/new; which renders, and does declaration order matter?
answer
- not first match wins
- every route gets a score
- static beats dynamic beats splat
- order only breaks exact ties
basics
~20 sprojects/new renders, whatever the order. React Router ranks every candidate route by specificity, so a static segment outranks a dynamic one and a splat ranks lowest. Declaration order only decides between siblings with identical scores.
solid answer
~30 sReact Router does not take the first route that matches. It flattens the route tree into branches, scores each full path and tries them from the highest score down. Static segments score most, dynamic segments like `:projectId` less, and a splat `*` carries a penalty, so `projects/new` beats `projects/:projectId`, which beats `projects/*`, regardless of where they are declared. Declaration order is only a tie-breaker between siblings whose scores are identical. The consequence to mention in an interview: a project whose id is literally `new` can never be reached at `/projects/new`, so ids must not collide with static sibling names.
code
tsx · 8 linesimport { createBrowserRouter } from "react-router";
// Declaration order does not change the winner for /projects/new.
export const router = createBrowserRouter([
{ path: "projects/:projectId", element: <Project /> },
{ path: "projects/new", element: <NewProject /> }, // wins for /projects/new
{ path: "*", element: <NotFound /> }, // lowest score: only when nothing else matches
]);go deeper
Recall that React Router picks the most specific matching route, so a static path like projects/new beats projects/:projectId regardless of order.
Explain the scoring: static over dynamic over splat, scores computed on the full branch path, and order used only to break ties between equal siblings.
Spot the id-collision bug where a static sibling shadows a dynamic segment, and design URL namespaces so reserved words never overlap with ids.
Treat the URL namespace as an API: reserve action words, choose id formats that cannot collide, and keep routing unambiguous as features are added.
## First match versus best match Older routers, including React Router v5's `<Switch>`, rendered the **first** route in declaration order that matched, so `/projects/:projectId` placed above `/projects/new` would swallow the "new" page and teams wrote routes in a careful order to avoid that. React Router v6 replaced this with **ranked matching**, and v7 keeps it: every route is scored by how specific its path is, and the most specific match wins. ## How the score is built When the router is created, it flattens the nested route tree into **branches**, one per route, each holding the chain of routes from the root down to it, and computes a score for the branch's full path. In the v7 source the weights are: | Segment kind | Contribution | |---|---| | static segment (`projects`, `new`) | +10 | | dynamic segment (`:projectId`) | +3 | | empty segment (the root `/`) | +1 | | index route | +2 | | path containing `*` | -2 penalty, and the `*` itself adds nothing | The score also starts from the number of segments. The exact numbers are an implementation detail; the ordering they produce is the contract: 1. **static beats dynamic**: `projects/new` beats `projects/:projectId`; 2. **dynamic beats splat**: `projects/:projectId` beats `projects/*`; 3. **longer, more specific paths beat shorter ones** built from the same kinds of segment. Branches are sorted from highest score to lowest, and the URL is tested against them in that order. The first branch whose pattern matches the whole URL wins. ## Where order still matters If two routes are **siblings** and their scores are **identical**, the router falls back to declaration order and tries the earlier one first. The source describes this as giving developers fine-grained control when they have routes with the same path shape. For routes that are not siblings, equal scores are simply treated as equal. In practice you meet the tie-break when two siblings have the same pattern shape, for example two dynamic routes at the same depth. ## Consequences in a real app - **You can declare routes in any readable order.** Grouping by feature is safe; you do not need to hoist static routes above dynamic ones. - **Reserved words shadow ids.** Because `projects/new` always wins, a project whose id is literally `new`, `edit` or `settings` cannot be reached through `/projects/:projectId`. Use opaque ids or slugs that never collide with static sibling segments, or move actions under their own prefix. - **A catch-all is naturally last.** A top-level `*` route scores lowest, so it only renders when nothing more specific matches; it is the standard not-found route. - **Nested routes are ranked as full paths.** The score belongs to the whole branch, not only the last segment, so specificity is compared on the entire URL pattern. ## Worked example With these siblings under `/`: ```tsx [ { path: "projects/:projectId", element: <Project /> }, { path: "projects/*", element: <ProjectsFallback /> }, { path: "projects/new", element: <NewProject /> }, { path: "*", element: <NotFound /> }, ] ``` - `/projects/new` renders `NewProject`, although it is declared third. - `/projects/42` renders `Project`. - `/projects/42/archive` renders `ProjectsFallback`, because no other route matches two segments after `projects`. - `/billing` renders `NotFound`. ## Explaining it in an interview A good answer has three parts. First, the rule: React Router picks the **most specific** matching route, not the first one declared. Second, the ordering that follows: static segments beat dynamic ones, dynamic beats splat, and the top-level catch-all only wins when nothing else matches. Third, the consequence: order only matters for exact ties between siblings, and static sibling names shadow dynamic ids. If the interviewer pushes further, mention that the route tree is flattened into scored branches and sorted before matching; a data router does this when it is created (and again if its routes change), so matching a URL is a walk down an already sorted list. The exact weights are internal and could change between releases; the ordering they produce is what applications rely on. ## Summary - Matching is by specificity score, not by position. - Static > dynamic > splat; the catch-all ranks last. - Declaration order only breaks ties between equal-scoring siblings. - Static sibling names shadow dynamic ids with the same text.
- In React Router v7, a user creates a project with the slug "new". Why can nobody open it, and what do you change?`/projects/new` always matches the static `projects/new` route because static segments outrank dynamic ones, so the project page is unreachable at that URL. Either forbid slugs that equal static sibling names, use opaque ids, or move actions under a separate prefix such as `/new/project`, so the project namespace has no static siblings.
- In React Router v7, where should a not-found route go in the route list?Anywhere. A top-level `path: "*"` route has the lowest score, so ranking tries it after every more specific route. It renders only when nothing else matches the URL. Nested splats can provide section-specific not-found pages, since a splat inside `projects` still outranks the top-level one for URLs under `/projects`.
saying these in an interview costs you the question
- The first route declared that matches the URL always wins
- Dynamic routes must be listed after static routes or they swallow them
- A splat route outranks a dynamic segment because it matches more
- Declaration order never affects matching in React Router v7
- The catch-all route has to be the final entry in the list