skip to content

In Vue Router 5, why does push({ name: 'settings' }) show an empty settings pane while reloading /settings shows the overview child?

level: middleimportance: should knowfreq 42%

answer

  1. empty-path child fills the pane
  2. the name decides which record matches
  3. path matching prefers the child
  4. name the child, not the parent
  5. a parent may have no component

basics

~20 s

The name sits on the parent, so a named push matches only the parent and its RouterView stays empty, while a path match prefers the empty-path child. Put the name on the empty-path child instead.

solid answer

~40 s

An **empty-path child**, `{ path: '', component: SettingsOverview }`, renders in the layout's `RouterView` when the parent's own URL is visited. Loading `/settings` by path matches that child, because a child with the same path is ranked ahead of its parent. A **named** push goes straight to the record carrying the name; if `settings` is on the parent, only the parent matches and the pane is empty. The docs' advice is to **name the child**: `children: [{ path: '', name: 'settings', component: SettingsOverview }]`, so every way of reaching `/settings` shows the overview. In development the router warns when a named parent has an unnamed empty-path child. Since 4.5 a parent and child cannot share a name, and since 4.1 a parent may omit its component to group routes without a layout.

code

ts · 11 lines
ts
const routes = [
  {
    path: '/settings',
    component: () => import('@/views/settings/SettingsLayout.vue'),
    children: [
      // the name lives on the empty-path child
      { path: '', name: 'settings', component: () => import('@/views/settings/SettingsOverview.vue') },
      { path: 'profile', name: 'settings-profile', component: () => import('@/views/settings/SettingsProfile.vue') },
    ],
  },
]

go deeper

for a junior

Know that an empty-path child fills the layout's pane when the parent URL is visited.

for a middle

Explain why path matching picks the empty child while a named push picks the named record, and name the child.

for a senior

Use component-less parents for shared prefixes and options, and avoid relative redirects inside children.

for a principal

Set naming conventions for nested route tables so every section is reachable by name the same way it is by URL.

## Two routes, one URL With an empty-path child, two records produce the same path: ```ts { path: '/settings', name: 'settings', // the problem component: SettingsLayout, children: [ { path: '', component: SettingsOverview }, { path: 'profile', component: SettingsProfile }, ], } ``` - The parent's path is `/settings`. - The empty child's path joins to `/settings` too. Both are matchable, and they reach the screen by different mechanisms. ## Matching by path versus by name | Navigation | Record matched | `route.matched` | Pane shows | |---|---|---|---| | reload or paste `/settings` | the empty-path child | parent, child | `SettingsOverview` | | `push('/settings')` | the empty-path child | parent, child | `SettingsOverview` | | `push({ name: 'settings' })` | the parent itself | parent | nothing | When matching by **path**, the router keeps records sorted by score, and a child whose score equals its ancestor's is inserted **ahead** of that ancestor. The empty-path child therefore wins. When navigating by **name**, there is no matching step: the router looks up the record with that name. If the name is on the parent, the parent is the deepest matched record, and the layout's `RouterView` has nothing at depth 1 to render. The docs describe exactly this: naming the parent lets you navigate to it without the nested route, but **a reload always shows the nested child**, because the reload is a path navigation. ## The fix: name the child ```ts children: [ { path: '', name: 'settings', component: SettingsOverview }, { path: 'profile', name: 'settings-profile', component: SettingsProfile }, ] ``` Now `push({ name: 'settings' })` and a reload both land on the overview. The router backs this up in two ways: 1. In development it **warns** when a route with a name has a child with no name, an empty path and no children of its own, since using the parent's name will not render that child. 2. Since Vue Router 4.5 it **throws in development** when a child or descendant is given the same name as an ancestor, so you cannot fix this by copying the name to both. Keep a name on the parent only if you deliberately want a way to show the layout with an empty pane. ## The empty-path child's URL A named empty-path child resolves **without** a trailing slash: `router.resolve({ name: 'settings' }).href` is `/settings`. That has a side effect for relative redirects inside children: `{ path: '', redirect: 'profile' }` resolves relative to `/settings`, which gives `/profile`, not `/settings/profile`. Redirect to a name, or to an absolute path, instead. ## A parent with no component Since Vue Router 4.1, a parent record may omit `component` and `components`: ```ts { path: '/settings/admin', meta: { requiresAdmin: true }, children: [ { path: 'users', component: AdminUsers }, { path: 'audit', component: AdminAudit }, ], } ``` - The parent still contributes its **path prefix**, and anything else declared on it, such as meta fields or a per-route guard, applies to all children. - `RouterView` **skips** a matched record with no components, so the children render in the `RouterView` that would otherwise have shown the parent. No extra layout level is added. - A parent with no component, no name and no redirect is not itself matchable: visiting `/settings/admin` directly falls through to other routes, such as a catch-all, unless you add an empty-path child. ## Proving it in a test `router.resolve()` shows the difference without mounting anything: 1. `router.resolve({ name: 'settings' }).matched.length` is `1` when the name is on the parent: only the layout record. 2. `router.resolve('/settings').matched.length` is `2`: the layout and the empty-path child. 3. After moving the name to the child, both calls return `2`. A test asserting that the two agree catches the mistake the moment someone moves a name. ## Summary of rules - Fill a layout's default pane with an empty-path child. - Put the name on that child. - Never reuse a name between a parent and its descendants. - Omit the parent's component when you only need a shared prefix or shared route options.

  • Inside the settings children, { path: '', redirect: 'profile' } sends users to /profile. Why?
    An empty-path child resolves to `/settings` without a trailing slash, and the relative location `profile` against `/settings` is `/profile`, just as a relative link would resolve. Redirect to a name, `{ name: 'settings-profile' }`, or to the absolute `/settings/profile`.
  • When is a parent record without a component the right tool?
    When several routes share a URL prefix and route-level options, such as meta fields or a guard, but no shared layout. `RouterView` skips the component-less level, so children render where the parent would have. If the routes do share visible chrome, give the parent a layout component instead.

saying these in an interview costs you the question

  • Named navigation and path navigation always match the same record.
  • Put the name on the parent so the whole section is reachable by name.
  • Giving the parent and the empty-path child the same name fixes it.
  • A parent without a component cannot have children.
  • An empty-path named child resolves to /settings/ with a trailing slash.