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:

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>

Notice we didn’t have to import anything. Installing the router plugin made this component (and a few others) globally available in our application without explicit imports

Load 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.vueout 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.

Funnily enough, the size of index.js actually increased a tiny bit: this is because this app is so small that the new runtime code generated to handle the lazy loading is actually bigger than the savings! But on real world apps code splitting makes a huge difference.

Recap

Further Reading

Bonus Exercises