In Vue Router 5, how do you build a /settings area whose sidebar layout stays on screen while /settings/profile and /settings/security swap inside it?
answer
- children array on the parent record
- a RouterView inside the layout
- relative child paths
- leading slash means root
- depth picks the matched record
basics
~10 sGive the /settings record a SettingsLayout component and a children array with relative paths like 'profile'; SettingsLayout renders the sidebar plus its own RouterView, which shows the matched child while the layout stays mounted.
solid answer
~40 sMake `/settings` a parent record with `component: SettingsLayout` and `children: [{ path: 'profile', ... }, { path: 'security', ... }]`. Child paths are **relative**: they join onto the parent to form `/settings/profile`; a child path starting with `/` is treated as a root path instead. `SettingsLayout` renders the sidebar and a `<RouterView />`. Each `RouterView` renders one level of `route.matched`: the one in `App.vue` shows `SettingsLayout`, the nested one shows the matched child. Switching from profile to security changes only the inner level, so the layout instance and the sidebar's local state survive. Visiting plain `/settings` matches only the parent, so the inner `RouterView` renders nothing until you add an empty-path child.
code
ts · 16 linesimport { createRouter, createWebHistory } from 'vue-router'
import SettingsLayout from '@/views/settings/SettingsLayout.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/settings',
component: SettingsLayout, // renders <SettingsSidebar /> and <RouterView />
children: [
{ path: 'profile', component: () => import('@/views/settings/SettingsProfile.vue') },
{ path: 'security', component: () => import('@/views/settings/SettingsSecurity.vue') },
],
},
],
})go deeper
Know the children option, relative child paths, and that the parent component needs its own RouterView.
Explain how RouterView depth maps onto route.matched and why the layout instance survives child switches.
Spot designs that remount layouts, such as flattened routes or over-broad keys, and keep shared data loading at the right level.
Decide which URL segments map to persistent layouts so each area keeps state and loads shared data once.
## The shape of a nested route In Vue Router 5, nesting is declared with the `children` option, which is just another array of route records: ```ts const routes = [ { path: '/settings', component: SettingsLayout, children: [ { path: 'profile', component: SettingsProfile }, { path: 'security', component: SettingsSecurity }, ], }, ] ``` - A child's `path` is **relative** to its parent: `profile` becomes `/settings/profile`. The router joins them with a slash. - A child path that **starts with `/`** is treated as a root path. `{ path: '/security' }` under `/settings` still renders inside `SettingsLayout`, but its URL is `/security`. The docs present this as a feature: component nesting without URL nesting. - Children can have children of their own; the depth is unlimited. ## Where each component renders The layout component contains its own `RouterView`: ```vue <template> <div class="settings"> <SettingsSidebar /> <RouterView /> </div> </template> ``` When the URL is `/settings/profile`, `route.matched` holds two records: the parent and the child. Each `RouterView` knows its **depth** by counting the `RouterView` ancestors above it: | RouterView | Depth | Renders | |---|---|---| | in `App.vue` | 0 | `SettingsLayout` | | inside `SettingsLayout` | 1 | `SettingsProfile` | A `RouterView` whose depth has no matched record renders nothing. That is why visiting plain `/settings` shows the sidebar with an empty pane: only the parent matched. ## What stays mounted Going from `/settings/profile` to `/settings/security`: 1. Depth 0 still matches the same parent record and the same `SettingsLayout` component, so Vue **patches** that instance; its `setup` does not run again. 2. Depth 1 changes from `SettingsProfile` to `SettingsSecurity`, a different component, so the old one unmounts and the new one mounts. The sidebar's scroll position, an expanded group or a search box inside `SettingsLayout` therefore survive child switches. Data that the layout loads once, such as the user's account summary, is loaded once for the whole area. ## Common mistakes - **Forgetting the nested `RouterView`.** The URL changes, `route.matched` is right, but nothing renders in the pane because no `RouterView` exists at depth 1. - **Writing child paths with the parent prefix.** `{ path: 'settings/profile' }` under `/settings` produces `/settings/settings/profile`. - **Accidental root paths.** `{ path: '/profile' }` in `children` silently leaves the settings URL space. - **Flattening instead of nesting.** Declaring `/settings/profile` and `/settings/security` as top-level records, each rendering the sidebar itself, remounts the sidebar on every switch, because depth 0 now holds a different component each time. - **Keying the top-level view on the full path.** A `:key` bound to the whole path on the outer view remounts the layout on every child switch, undoing the benefit. ## Seeing the levels in code `route.matched` lists the records that matched, from the outermost to the innermost: - On `/settings/profile` it holds the settings parent and the profile child. - Each entry carries the record's path pattern, its components and its meta fields. - Layout-level UI such as a breadcrumb or a section title can walk this list instead of parsing the URL. - `route.matched.length` tells the layout whether any section is selected, which is how it can show a hint in an empty pane. A unit test can prove the layout persists: mount the app with a memory-history router, push `/settings/profile`, expand a sidebar group, push `/settings/security`, and assert the group is still expanded while the security pane rendered. ## Beyond one level - Params from the parent path are available in every child: under `/settings/:section`, a child reads `route.params.section`. - A parent record may omit its component entirely; its `RouterView` level is then skipped, which groups routes under a prefix without adding a layout. - Several panes at the same level, such as a list and a detail beside each other, use **named views** rather than more nesting. - The empty-path child fills the pane when the parent's own URL is visited. - Each extra level needs its own `RouterView` inside the component of the level above; three levels of `children` mean three `RouterView`s in three components. - Named routes work at any depth: `router.push({ name: 'settings-profile' })` builds `/settings/profile` from the joined paths, so links survive a later rename of the parent's path.
- Why does the sidebar keep its expanded state when switching from profile to security?Both URLs match the same parent record at depth 0, so the outer `RouterView` renders the same `SettingsLayout` component and Vue patches the existing instance. Only the depth-1 `RouterView` swaps components. Local state in the layout, including the sidebar's, is never destroyed.
- A teammate puts { path: '/billing', component: SettingsBilling } inside the settings children. What happens?A child path starting with `/` is absolute, so the URL is `/billing`, not `/settings/billing`, but it still renders inside `SettingsLayout`. That is useful for nesting components without nesting URLs; if it was meant to be a settings URL, drop the leading slash.
saying these in an interview costs you the question
- Child paths must repeat the parent prefix, like 'settings/profile'.
- A child path starting with / still gets the parent prefix.
- The layout remounts on every child switch, so its state is always lost.
- One top-level RouterView renders every level of nested routes.
- Visiting /settings automatically renders the first child.