Skip to content

Remove the deprecated component, components and props options - #800

Open
pleek91 wants to merge 23 commits into
mainfrom
remove-deprecated-views
Open

Remove the deprecated component, components and props options#800
pleek91 wants to merge 23 commits into
mainfrom
remove-deprecated-views

Conversation

@pleek91

@pleek91 pleek91 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Description

Removes the deprecated component and components options and the props argument. Views are added with addView.

Breaking change

// before
createRoute({
  name: 'user',
  path: '/user/[id]',
  component: UserPage,
}, (route) => ({ id: route.params.id }))

// after
createRoute({
  name: 'user',
  path: '/user/[id]',
})
.addView(UserPage, {
  props: (route) => ({ id: route.params.id }),
})

Named views move from components to a name per view.

// before
createRoute({
  name: 'user',
  path: '/user/[id]',
  components: { default: UserPage, sidebar: UserSidebar },
}, {
  default: (route) => ({ id: route.params.id }),
  sidebar: (route) => ({ id: route.params.id }),
})

// after
createRoute({
  name: 'user',
  path: '/user/[id]',
})
.addView(UserPage, {
  props: (route) => ({ id: route.params.id }),
})
.addView(UserSidebar, {
  name: 'sidebar',
  props: (route) => ({ id: route.params.id }),
})

Passing props without a component is no longer supported. A view needs a component to render.

Passing component or components is now an error rather than being ignored, so the compiler points at anything left to migrate.

@pleek91
pleek91 marked this pull request as ready for review July 30, 2026 15:58
@pleek91
pleek91 force-pushed the remove-deprecated-views branch from 53a0204 to 51caa6d Compare July 30, 2026 16:33
pleek91 and others added 16 commits July 30, 2026 11:34
Views were spread across three parallel records — components, props, and
prefetch — with props further split between a bare getter for a lone unnamed
view and a record once a named view was added. Merge them into one record of
{ component, props, prefetch } keyed by view name.

The props argument's two shapes are normalized once in createRouteViews, so
nothing downstream deals with them. That removes ViewProps, isBareProps,
isPropsRecord, toPropsRecord, addProps, addPrefetch, and collapses
getComponentProps to a single map.

The two duplicate parent props derivations — one for the createRoute props
argument, one for addView — are now a single shared ViewsPropsReturnType.
A lone unnamed view still gives its props directly rather than under a
'default' key, so the public shape of parent.props is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assertions targeted the old props slot, which held either a bare getter or a
record. They now target the views record, so a lone unnamed view reads as
{ default: RouteView<getter> } rather than the getter alone.

Adding a view without a getter no longer adds a key to the type. It used to
add one carrying never, which disagreed with the createRoute path, where a
component with no props produced an empty record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each entry was { id, views }, so reaching a view read route.views[depth].views
.sidebar. The id was documented as always equal to matches[depth].id, and every
consumer already had the depth or the match in hand, so it was duplicated data
paying for a level of nesting.

The entry is now the record itself: route.views[depth].sidebar. Consumers take
the id from matches, which also removes the RouteViews unwrapping the parent
props types had to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Views were a second tuple indexed in parallel with matches, which the two had
to be kept aligned by hand. That alignment is what made getParentContext read
the wrong parent: it took matches.at(-2) and views.at(-2) independently. Views
now hang off the match they belong to, so there is no alignment left to get
wrong and route.matches shows names, meta and view types together.

matched is derived from the last match by withMatched, which everything that
builds or rebuilds a route goes through. The Route type already described it as
LastInArray<TMatches>, so this makes the runtime agree with the type rather
than storing it twice. combineRoutes no longer reads child.matched either, so
matched is only ever an output.

Views were also the one part of a route that was not markRaw, so passing
through reactive() in createRouterRoute turned every component into a reactive
proxy, which Vue warns about. They are inside the markRaw match now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
matched on a route definition meant nothing — nothing has been matched yet, it
was just the last of the chain. Every reader was already working with a resolved
or router route, and ResolvedRoute declares its own fields rather than extending
Route, so it can derive matched from matches instead of forwarding it.

It is computed once in createResolvedRoute, which runs after every addView, so
there is no window where it can disagree with matches. That removes the need for
withMatched and the derivation it was doing at all four construction sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each match kept the parent option, which is a whole Route including its chained
methods, so a child's match embedded the full type of every ancestor above it.
Nothing reads parent off a match, and matches already carries the ancestors it
describes.

Also drops the two matched.meta type tests, which became duplicates of the
matches[number].meta block below them, and points the router immutability
assertion at matches.at(-1) rather than a route definition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
addView left a view out of the type entirely when it had no props getter, so a
route with a default and a sidebar view alongside one that took props showed
only the one with props. That was fine while the views type existed purely to
type parent props, but it is now the views a user sees on route.matches, so it
should describe every view.

A view without a getter reads as RouteView, and parent props still only sees
views that have one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every addView rebuilt the route by re-deriving its url from the previous route
type, so chaining nested one inside the last: three calls produced three layers
of WithViewProps, each carrying the views accumulated so far. A route ended up
describing its own history rather than its current shape.

RouteAddView and RouteWithMethods now take the url and matches, so adding a view
passes the url through untouched and only replaces the last match's views. The
result is the same shape whether one view was added or ten, and WithViewProps is
gone along with the Pick that recovered the url.

Also rewrites the route shape type test. It asserted hooks against the route
type itself, which only held for createRoute({}) because empty options collapse
to a bare Route — it failed as soon as a view was added, though every member was
present. It now asserts the members a route has, against a plain route and each
addView shape, since those should not differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
createRoute described its return as a four way intersection of the route with
each group of methods, while a route rebuilt by addView was one alias. Both are
now RouteWithMethods, so a route reads the same however it was built.

The hooks were the reason this needed the long form: they took the context
extracted from the options rather than from the route. Context is derived from
matches, so the route carries it either way.

Also stops PropsToViews distributing over its type parameter, so never cannot
collapse a views record to never.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The caller assembled the match around a views helper and reapplied markRaw
itself, which left the reason for the marking away from the code that needed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A child is given a parent's props directly when the unnamed view is the only one
with props, and keyed by view name otherwise. Views used to store that
distinction, so reading it was a matter of asking whether props was a function or
a record. They are always keyed by name now, so the question is asked of them
instead — behind guards that say which case they describe, as the old ones did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Indexing the last match said it was always there. A route without matches is a
broken route, so say so instead of typing around it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isWithComponentProps and isWithComponentPropsRecord told a bare props getter
apart from a record of them, which is how a parent's props shape used to be
read. Views are keyed by name now, so nothing asks that question of the options.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pleek91
pleek91 force-pushed the remove-deprecated-views branch from 51caa6d to 1156f5d Compare July 30, 2026 16:34
A view only needs the props type, not the signature that produced it, so
RouteView holds the getter's return type. Reading parent props back is a
single infer, and views read as RouteView<{ foo: string }> in tooltips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pleek91
pleek91 force-pushed the remove-deprecated-views branch from 1156f5d to b83562d Compare July 30, 2026 17:22
pleek91 and others added 6 commits July 30, 2026 12:52
The existing test gave the parent no views at all, which reports undefined
without ever reading a view. This one relies on RouteView's default, which is
what encodes "this view has no props".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every view comes from addView now, so nothing seeds views from options. That
takes the component and components options with it, along with the props
argument and the types that existed to describe it: CreateRouteProps,
RoutePropsRecord, RouterViewPropsGetter, CreateRouteWithProps, PropsToViews,
WithoutComponents, isWithComponent and isWithComponents.

createRouteViews had nothing left to build, so a route starts with no views and
the file keeps only the default view name and the two view guards.

Tests still use the removed syntax and are not converted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tests that named a component in options or passed a props argument now add a view
instead. Where a component was left out for brevity, one is passed, since props
without a component is no longer something the router supports.

The deprecated props argument had its own tests for whether the argument was
required and what shape it took. addView covers each of those, so they are
removed rather than rewritten.

Rejections build their route with addView too, and one test read a component off
the matched route, which views carry now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Leaving component and components out of the options type did not stop the old
syntax type checking — extra properties are allowed when inferring against it, so
a route naming a component silently came out with no views. Several tests were in
that state and only a render assertion caught it.

Declaring them means passing one is a mismatch rather than an extra property. The
declared type is the guidance, so the error says what to do instead of naming a
type nobody wrote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They only exist to catch the old syntax, so they should go at the next major
version once nobody is upgrading past them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Props are now only declared through addView, so move the props type tests
there and drop the duplicates. Also removes the two "backwards compatibility"
tests, which no longer test the deprecated options.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pleek91
pleek91 force-pushed the remove-deprecated-views branch from b83562d to 69d4261 Compare July 30, 2026 17:53
@pleek91

pleek91 commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

@stackoverfloweth not going to merge this until after we do the next minor release so we have one version where both are supported for users to use as a migration version.

Base automatically changed from views-shape to main July 31, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants