Routing
git checkout 4-routing
You’ll notice the home page has changed to show a new Session detail page. I’ve updated App.vue
to render SessionDetails.vue
instead of SessionList.vue
. In this exercise, we will install a router library and configure it to help the user navigate back and forth between the session list page and the session details page, which will dynamically load the details of the selected session. This is often called a Master-Detail View.
Installing Vue-Router
First, we need to install the vue-router
library, which is the de-facto standard for doing routing in Vue apps that need it. The command is: npm install --save vue-router
Now that we have the library downloaded, it's time to design our routes. We will have two routes to start:
/
- the home page will show the sessions list as it did before
/sessions/:id
will show more information about the session identified by the:id
route parameter. For example,/sessions/1234
would show the details about the session with id 1234
Configuring Routes
In Vue Router, you define the URL patterns and what component they should display when navigated to.
We’re going to do that in the src/main.ts
file:
We’ll need to import createRouter
and createWebHistory
from vue-router
. These are two factory functions that help us inject the router plugin into our app. Make sure you’re importing both SessionList
and SessionDetails
components.
import App from './App.vue';
+ import { createRouter, createWebHistory } from 'vue-router';
+ import SessionList from './components/SessionList.vue';
+ import SessionDetails from './components/SessionDetails.vue';
Add the following to src/main.ts
before the call to createApp
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: SessionList,
},
{
path: '/session/:id',
component: SessionDetails
},
],
});
createWebHistory
configures the plugin to use HTML 5 history mode. This uses the browser’s native History class to push and pop URLs from the history stack, letting users navigate back and forth through the app with the normal back button.
The routes
object is where we configure our route URLs. For each one, we provide a path
and a component
to show when the route matches the current browser URL. Vue understand the :id
placeholder syntax, and will match any URL that starts with /session/
followed by an identifier. It will show the configured component and make the identifier available.
The createRouter
function creates a Vue plugin that is configured with our routes and our desired history mechanism. Next we need to configure out application to use that generated plugin.
The app produced from createApp
has a use
method that will install plugins, and we’ll use that here:
- createApp(App).mount('#app');
+ createApp(App).use(router).mount('#app');
Placing the matched component
We’re done installing and configuring the router, but its still not working. Vue Router operates through a special component called the <router-view />
. This “opens up a hole” in the app into which vue-router can place the component matching the current URL. This allows you to mix static markup (like the header and foother) and wrap it around the dynamic, route-specific HTML.
Replace the <SessionDetails />
component in src/App.vue
with <router-view />
.
</nav>
- <SessionDetails />
+ <router-view />
</div>
router
plugin made this component (and a few others) globally available in our application without explicit importsLoad it in your browser! Now it shows the session list again on the homepage. We don’t have any links yet, but if you visit http://localhost:5175/session/537232 it should show the session details page.
Linking
This is nice, but we can’t expect users to type in URLs when they want to visit different pages.
We’re going to have to add a link from the session cards to the details page.
Helpfully, vue-router
provides a router-link
component we can use to trigger the navigation. It will render an HTML <a>
tag that will do the right thing. The router-link
component needs a :to
prop (sort of like an <a>
tag’s href
). The :to
prop can be a string or an object with a path
property. We’ll use the object syntax since we need to concatenate the /session/
prefix with the session’s id.
Make the following change in SessionCard.vue
:
- <a href="" class="btn btn-primary">Details</a>
+ <router-link :to="{ path: '/session/' + session.id }" class="btn btn-primary">Details</router-link>
<router-link>
accepts many of the same props as a native <a>
tag. Just swap out href
for the :to
prop.You should now be able to click the Details
button on any session card and be taken to the details page. Unfortunately it only shows the details for this workshop no matter which one you click. We haven’t added logic to read the :id
from the current route and look up the correct data. That’s coming next.
Reading the data
If you look carefully at the onMounted
lifecycle hook in src/components/SessionDetails.vue
you’ll notice it makes a request for a hardcoded /api/sessions/537232.json
. We need to replace 537232
with the id of the session the user actually clicked on.
Remember the route definition included the :id
path parameter placeholder: in main.ts
we configured this route as path: "/sessions/:id"
. When matching a URL, Vue Router will pluck that out and make it available to the component that handles the route.
We can get access to data about the current route through the useRoute
function.
Import useRoute
from vue-router
and call it in the component. Store the result in a local variable called route
:
import { useRoute } from 'vue-router';
const route = useRoute();
The route
object provides us with details about the current URL. One of those nice properties is the params
object which includes properties for every dynamic route segment in our route definition. Since this route had the :id
placeholder in it, we can expect route.params.id
to hold the id that was given in the link.
We’ll use that to make a more dynamic API call that corresponds to the requested session:
onMounted(() => {
- fetch(`/api/sessions/537232.json`)
+ fetch(`/api/sessions/${route.params.id}.json`)
.then((res) => res.json())
Once you’ve made this change, the data should load correctly and show you the details of the session you actually clicked on. Navigate back and forth between the session list and different sessions. Make sure the browser’s back and forward buttons work as a user would expect.
Manual navigation
Links are a great way to navigate. Anytime you have a button or element who’s primary intent is to navigate, you should use an <a>
tag via <router-link>
.
But sometimes you need to navigate programmatically. A common scenario is after saving a form: you need to wait for the save to complete and then automatically navigate to the next page.
Navigation in vue-router
is done through the router instance. You can get access to the router through the useRouter
function. Lets say you wanted to navigate to the home page, once you had a router in hand you could call router.push('/')
to push the homepage to the top of the navigation stack.
Add useRouter
to the import list from vue-router
and then add the following to the <script>
block of the SessionDetails.vue
component:
const router = useRouter();
function handleBack() {
router.push('/');
}
Try this: add a button somewhere in the template and wire its click handler up to the handleBack
function we just made.
Lazy Loading
The larger your app gets, the more code you have, and the larger your compiled javascript bundle. Large bundles can take a long time to download, and load up code the user might not ever use. If they never visit the SessionDetails
page, why should they load the code for it?
The javascript ecosystem supports “lazy loading” and “module splitting”. With the right syntax, we can teach vue-router
to lazily load the SessionDetails code on demand.
First, lets show what it looks like if you build for production right now.
Run npm run build
.
Your output should look something like this:
$ npm run build
> codemash-schedule-vue@0.0.0 build
> vue-tsc && vite build
vite v5.0.7 building for production...
✓ 36 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index--LRus7id.css 232.59 kB │ gzip: 31.21 kB
dist/assets/index-Yl_EIoSd.js 83.13 kB │ gzip: 32.89 kB
✓ built in 774ms
You can see that vite
generated one javascript file. For me its about 83KB. If my server can do gzip compression, its about 32kB. Now this is pretty tiny in the world of javascript apps, but imagine when we had dozens of screens, hundreds of components, and multiple third party libraries. You can see how this would grow.
We’d like to code-split so that the code for the SessionDetails page isn’t loaded until you actually visit it.
We can use javascript’s dynamic import statement for this. Update the main.ts
file as follows:
import App from './App.vue';
import { createRouter, createWebHistory } from 'vue-router';
import SessionList from './components/SessionList.vue';
- import SessionDetails from './components/SessionDetails.vue';
const router = createRouter({
history: createWebHistory(),
@@ -14,7 +13,7 @@ const router = createRouter({
},
{
path: '/session/:id',
- component: SessionDetails,
+ component: () => import('./components/SessionDetails.vue'),
},
],
});
We basically remove the import from the top of the file down to the middle. Instead of the route definitions’ component
being a direct reference to the SessionDetails
component, it is a function that downloads the component when invoked. Its loaded lazily.
vite
will do module splitting: it will pull SessionDetails.vue
out to its own file called a “chunk”. If it has any dependencies that are not shared with other chunks, those will be bundled with it. Its pretty smart!
Confirm it still works in the browser, then run npm run build
again to see the difference.
$ npm run build
> codemash-schedule-vue@0.0.0 build
> vue-tsc && vite build
vite v5.0.7 building for production...
✓ 37 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/SessionDetails--fRq7Hws.css 0.17 kB │ gzip: 0.12 kB
dist/assets/index-V_-fRiN-.css 232.43 kB │ gzip: 31.17 kB
dist/assets/SessionDetails-yStA9csd.js 1.36 kB │ gzip: 0.71 kB
dist/assets/index-ktOqLjnO.js 83.17 kB │ gzip: 33.10 kB
✓ built in 868ms
This time we can see there are new chunks for SessionDetails: its javascript code and its css styling.
Recap
- You learned how to add Vue Router to an application
- You learned how to define routes so that users can navigate between pages
- You learned how to read dynamic data from the current route
- You learned how to render links using
<router-link>
- You learned how to navigate programmatically through
router.push
- You learned how to configure code-splitting to minimize bundle sizes and improve load time
Further Reading
Bonus Exercises
- Make
SessionList
lazily loaded as well. How many javascript bundles are created when you runnpm run build
?
- the header has a link to Sessions but it is broken. Can you fix it to take you to the sessions list page?
- Make the title of each session card also link to the detail page
- As a conference attendee, i’d like to see all of a speaker’s sessions on one page. Create an page to show author details, including their biographical information and all of their sessions
- Speaker details are in in the
public/api/speakers/
folder
- Use a link from the author name on the session list page
- Reuse the SessionCard component to list their sessions
- Speaker details are in in the