Nested routes and layouts
Share a frame between pages with child routes and a nested RouterView, load pages only when visited, keep old addresses working with redirect, and wrap pages in Transition and KeepAlive through RouterView's slot.
What you'll learn
When you finish, you can do these.- 1Share one frame between pages with child routes
- 2Write child paths, a default page and a redirect
- 3Load a page only when someone first opens it
- 4Fade between pages and keep what was typed in them
Picture the settings area of an app: a Settings heading, tabs for Overview, Profile and Security, and a panel under them that shows the open tab. Each tab needs its own address, but a click should change only the panel. Built as three separate pages, each would copy the heading and the tabs, the frame around the panel, so one change to the frame means three files to edit.
You need one idea from Your first router, which opens this track: a route pairs an address with a component, such as /about with the About page, and <RouterView /> marks where that component appears. Nested routes put the frame in a parent route and each tab in a child route shown inside it. That is how the router does layouts, frames that several pages share. The Code tab holds this settings area: the routes in router.ts, the frame in pages/SettingsLayout.vue, a page per tab, and App.vue with a top menu and an address line that prints the current address. By the end you add a Notifications tab to the frame.
Child routes render inside their parent
A route can have children, a routes list of its own, so the settings area becomes one parent route with a child per tab. The parent's component draws the frame: SettingsLayout.vue holds the heading and the tabs, and puts its own <RouterView /> under them, where a tab's page goes.
In router.ts, the settings route names SettingsLayout as its component and lists one child per tab, and an /account route follows it:
{
path: '/settings',
component: SettingsLayout,
children: [
{ path: '', name: 'settings', component: SettingsOverview },
{ path: 'profile', name: 'profile', component: Profile },
{ path: 'security', name: 'security', component: () => import('./pages/Security.vue') },
],
},
{ path: '/account', redirect: '/settings' },
At /settings/profile, the RouterView in App.vue shows the parent's component, the frame, and the frame's own RouterView shows the matched child, Profile. Each RouterView renders one level of the match.
Child paths are relative: profile under /settings is /settings/profile.
A child with an empty path is the frame's default page, shown at the parent's own address, so /settings opens the overview instead of an empty frame. The name settings goes on that empty child, not on the parent: a link to a named parent renders the frame with nothing inside, and Vue Router warns about such a route in development.
redirect sends one address to another. The /account route keeps old bookmarks working: the top menu's last link, An old bookmark, goes to /account and lands on /settings. A redirect on the parent, redirect: { name: 'profile' }, would skip the overview and send /settings straight to the Profile tab.
In the top menu, Settings stays bold on every settings page. RouterLink gives the class router-link-active, which App.vue makes bold, to the link to the current route and to a link to its parent.
Load pages only when they are visited
Every visitor downloads the pages imported at the top of router.ts, such as Profile, with the rest of the app. Few visitors open Security, so its route has component: () => import('./pages/Security.vue'), a lazy route: instead of a component, the router gets a function that imports one.
In a build, Vite, the build tool, turns that import() into a separate file. The first time someone opens Security, the router calls the function, waits for the file, then shows the page. Visitors who never open Security never download it, so the first screen loads faster.
Never wrap a route component in defineAsyncComponent(), Vue's own way to load a component later. The router already does the loading: it waits for the file before it changes the page, and the wrapper skips that wait. In development, Vue Router warns Component "default" in record with path "/settings/security" is defined using "defineAsyncComponent()". That function is for parts of a page, such as a heavy chart.
Transitions and kept state with RouterView's slot
To fade the panel between tabs, and to keep what was typed in Profile, you wrap the page that RouterView shows. <RouterView> has a scoped slot: your own markup goes between its tags, and v-slot on the tag receives what RouterView hands you, here Component, the matched page.
<component :is="Component" />, Vue's built-in tag that renders whichever component it is given, draws that page, so you can wrap it in anything. This is the frame's RouterView in SettingsLayout.vue:
<RouterView v-slot="{ Component }">
<Transition name="fade" mode="out-in">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</Transition>
</RouterView>
<Transition> adds CSS classes while one page leaves and the next enters. The fade- classes in the frame's style block, named after name="fade", fade the panel, and mode="out-in" lets the old page leave first.
<KeepAlive> keeps a page you leave in memory instead of removing it. Type a display name in Profile, open Security and come back: the name is still there. Keep Transition outside KeepAlive, as here.
What goes wrong
Two common mistakes leave the frame on screen. The first is a leading slash, which makes a child's path absolute.
{ path: '/profile', name: 'profile', component: Profile },
Click Profile and the page still opens inside the frame, but the address line reads /profile. A path that starts with / ignores the parent's path, even inside children, so bookmarks to /settings/profile now match nothing. Write child paths without the slash.
{ path: 'profile', name: 'profile', component: Profile },
Profile opens at /settings/profile again.
The second is a parent with no RouterView, so its children have nowhere to go.
<section class="frame">
<h1>Settings</h1>
<nav class="tabs">
<RouterLink :to="{ name: 'profile' }">Profile</RouterLink>
</nav>
</section>
Click Profile: the address line changes, but the panel stays empty. Nothing warns: the router matched the child, and no RouterView asked for the second level.
<section class="frame">
<h1>Settings</h1>
<nav class="tabs">
<RouterLink :to="{ name: 'profile' }">Profile</RouterLink>
</nav>
<RouterView />
</section>
With a RouterView under the tabs, each tab's page fills the panel again.
What to remember
- A parent route's component is the frame, and its own
<RouterView />shows the matched child. Each RouterView renders one level of the match. - Child paths are relative, and a leading slash makes a child absolute. A child with an empty path is the default page at the parent's address; give that child the name.
redirectsends one address to another, for a default tab or for old bookmarks.() => import('./pages/Security.vue')makes a route lazy. Never wrap a route component indefineAsyncComponent().<RouterView v-slot="{ Component }">lets you wrap the page in<Transition>and<KeepAlive>. Never wrap RouterView itself.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Add a Notifications tab. In router.ts, add
{ path: 'notifications', name: 'notifications', component: () => import('./pages/Notifications.vue') }to the children. In SettingsLayout, add<RouterLink :to="{ name: 'notifications' }">Notifications</RouterLink>to the tabs. Click it: the address line reads /settings/notifications, the checkbox appears inside the frame, and Settings stays bold in the top menu. - Swap the overview for a redirect. In router.ts, delete the empty-path child and give the parent
name: 'settings'andredirect: { name: 'profile' }. Click Settings in the top menu: the address line jumps to /settings/profile. Click An old bookmark: it follows two redirects and lands on Profile too. Put the overview child back and remove the two lines. - Break it three ways, undoing each change before the next. In router.ts, import
defineAsyncComponentfromvueand change the security route tocomponent: defineAsyncComponent(() => import('./pages/Security.vue')). Open Security: the page appears, and the preview shows[VUE_ROUTER_R0029] Component "default" in record with path "/settings/security" is defined using "defineAsyncComponent()".In SettingsLayout, delete the whole<RouterView v-slot>block and click the tabs: the panel stays empty and nothing warns. Change the profile child's path to'/profile'and click Profile: the page opens inside the frame, but the address line reads /profile.