Route Tree Internals — compile · linearize · match

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Start with frontend/tanstack-start: file-based routing for the basics of file conventions — this bundle goes under the hood to the internal tree mechanics (compile → linearize → rank → match).

URL → component: never direct, always via the tree

A URL like /users/42/posts never reaches a component directly. TanStack Router turns file conventions (or createRoute() calls) into a parent-child route tree, flattens it into a ranked candidate list, and matches the URL segment-by-segment. The matched node's ancestor chain becomes the rendered route-context stack: [Root, Users, UserDetail, UserPosts].

stageinputoutput
1 · compile file convention: users.$id.posts.tsx tree node {'{ name:"UserPosts", path:"/users/$id/posts", parent:"UserDetail" }'}
2 · linearize the parent-child tree flat array of full paths, each carrying its ancestor chain
3 · rank flat array sorted by specificity — static(0) > $param(1) > $splat(2); longer beats shorter on ties
4 · match URL /users/42/posts first ranked candidate whose segments all match → {'{ route, params:{id:"42"} }'}

1 · the compiler you write (edit me)

2 · Babel compiles JSX → element tree

<App/> becomes React.createElement(App). Each render re-runs matchUrl(CANDIDATES, url) against the pre-built, pre-ranked candidate list — the same linearize-once / match-many shape TanStack uses at runtime.

// (hit "compile & render" to see Babel's output)

3 · live React (the matching pipeline, proven)

Type a URL (or click a quick button). The gold-check drives the input directly: /users/42/posts → asserts UserPosts + id=42, then /settings → asserts Settings. That proves ranking put the right candidate first and $id was sliced into params.

tree nodes: · gold: 6 nodes → /users/42/posts→UserPosts{id:42} → /settings→Settings

intent → pattern

intentpatternwhy
static beats dynamic /users/new before /users/$id rank 0 < rank 1 — static segment wins the same-length tie
capture one segment /users/$idparams.id $ matches exactly until the next /
capture the rest /files/$params._splat trailing $ is a splat/wildcard, consumes everything left
resolve ambiguity params: { priority: 10, parse } higher priority candidate tried first; parse()=false falls through
build the context stack matched node .chain[Root, …, leaf] each ancestor's layout/outlet wraps the next — the route-context chain