Categories
JavaScript Vue

Introduction to Vue.js Testing

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue.js apps by writing a simple app and testing it.

Getting Started

To get started, we create an app that gets a joke from the Chuck Norris Jokes API.

We start by creating an empty folder, going into it, and running the Vue CLI by running:

npx vue create .

In the wizard, we select Unit Tests, then choose Jest and then proceed.

Now that we have the files generated, we can change some code. We can delete the components folder and replace the code in App.vue with:

<template>  
  <div id="app">  
    <button @click='toggleJoke()'>{{jokeHidden ? 'Show' : 'Hide'}} Joke</button>  
    <p v-if="!jokeHidden">{{data.value.joke}}</p>  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data() {  
    return {  
      jokeHidden: false,  
      data: { value: {} }  
    };  
  },  
  beforeMount() {  
    this.getJoke();  
  },  
  methods: {  
    async getJoke() {  
      const res = await fetch("http://api.icndb.com/jokes/random");  
      this.data = await res.json();  
    }, 

    toggleJoke() {  
      this.jokeHidden = !this.jokeHidden;  
    }  
  }  
};  
</script>

<style>  
#app {  
  font-family: "Avenir", Helvetica, Arial, sans-serif;  
  -webkit-font-smoothing: antialiased;  
  -moz-osx-font-smoothing: grayscale;  
  text-align: center;  
  color: #2c3e50;  
  margin-top: 60px;  
}  
</style>

The code just gets a joke from the API and then display it. Also, it has a button to show and hide the joke.

Our app looks something like the following:

https://thewebdev.info/wp-content/uploads/2020/04/chuck.png

Creating the Tests

Now that we have something to test, we can actually write the tests.

In the tests/unit folder, we delete what we have then create app.spec.js in that folder.

Then we open the file we created and add:

import { mount } from '@vue/test-utils';  
import App from '@/App.vue'

const mockResponse = {  
  "type": "success",  
  "value": {  
    "id": 178,  
    "joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",  
    "categories": []  
  }  
}

To import the component that we’ll test, the mount function to let the Vue Test Utils build and render the component for testing, and the mockResponse object that we’ll use to set the mock data.

Then we add the skeleton for our test by writing:

describe('App.vue', () => {  
  beforeEach(() => {  
    jest.clearAllMocks()  
  })  
})

We have the string description for our test suite and a callback which we add out tests to.

Inside the callback, we have the beforeEach hook to clear all the mocks by running jest.clearAllMocks() .

We need this because we’ll mock some of the functions in our component later.

Adding our First Test

Next, we write our first test. This test will simulate getting the data from the API and then displaying the joke on the screen.

It won’t actually get the joke from the server since we want our test to run anywhere and at any time. Getting it from the server won’t let us do that.

The API returns something different every time we call it and also it might not always be available.

With that in mind, we write:

it('renders joke', async () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)  
  })

in the callback we passed into the describe function after the beforeEach call.

The test above calls mount on our App component to build and render the component and returns a Wrapper object to let us access it.

In the second argument, we pass in the options with the methods property so that we can mock the getJoke method with Jest with jest.fn(). We want to mock it so that our test doesn’t call the API.

Once we have the wrapper then we run:

wrapper.vm.data = mockResponse;

to set the mockResponse data to the data property of our component instance.

Once we did that, we check that we get the joke in our mockResponse rendered by writing:

expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)

since we put our joke in the p tag in our App component.

The expect method and toMatch are from Jest.

Writing Test that Interacts with UI Elements

Writing a test that does something to UI elements like buttons isn’t that much more work.

To test the button that we added to our app actually shows and hides the joke, we write:

it('toggles joke', () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Show Joke');  
    expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);  
  }
)

The first part:

const wrapper = mount(App, {  
  methods: {  
    getJoke: jest.fn()  
  }  
});  
wrapper.vm.data = mockResponse;

is the same as before. We mock the getJoke function with jest.fn() so that our test won’t call the API. Then set the mock data.

Next, we check the button text by writing:

expect(wrapper.find('button').text()).toMatch('Hide Joke');

and that our mocked joke is shown in the p element:

expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);

Then we click our button by running:

wrapper.find('button').trigger('click');

And then check for the text of the button and whether the p element is removed by our v-if directive:

expect(wrapper.find('button').text()).toMatch('Show Joke');  
expect(wrapper.find('p').exists()).toBe(false);

Finally, we can do the click again and check if the joke is shown again as follows:

wrapper.find('button').trigger('click');  
expect(wrapper.find('button').text()).toMatch('Hide Joke');  
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);

Running the Tests

Together, we have the following test code in app.test.js :

import { mount } from '@vue/test-utils';  
import App from '@/App.vue'

const mockResponse = {  
  "type": "success",  
  "value": {  
    "id": 178,  
    "joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",  
    "categories": []  
  }  
}

describe('App.vue', () => {  
  beforeEach(() => {  
    jest.clearAllMocks()  
  }) 

  it('renders joke', async () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)  
  }) 

  it('toggles joke', () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Show Joke');  
    expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
    expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);  
  })  
})

Then we run the tests by npm run test:unit .

We should get:

PASS  tests/unit/app.spec.js  
  App.vue  
    √ renders joke (19ms)  
    √ toggles joke (11ms)
Test Suites: 1 passed, 1 total  
Tests:       2 passed, 2 total  
Snapshots:   0 total  
Time:        2.102s  
Ran all test suites.

every time that we run our tests since we mocked the data.

Conclusion

Vue CLI creates a project that has unit testing built-in if we choose to include it. This saves us lots of work.

Jest is an easy test runner with lots of features like mocking and expect matchers that we can use.

To test UI components, we use the wrapper object returned by mount , which has the rendered component. Then we can use find to search the DOM for what we want to look for.

If the element exists, we can also trigger events on it by calling the trigger method with the event that we want to fire.

Finally, we have the exists method to check if the element we look for actually exists.

Categories
JavaScript React

Creating 404 and Recursive Paths with React Router

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

To build single-page apps, we have to have some way to map URLs to the React component to display.

In this article, we’ll look at how to create 404 routes and recursive paths with React Router.

404 Routes

We can create 404 routes to show something when none of the routes we added are matched.

To do this, we can use the asterisk as a wildcard character.

For example, we can write the following code;

import React from "react";  
import ReactDOM from "react-dom";  
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

function Foo() {  
  return <h3>Foo</h3>;  
}

function Bar() {  
  return <h3>Bar</h3>;  
}

function NotFound() {  
  return <h3>Not Found</h3>;  
}

function App() {  
  return (  
    <Router>  
      <div>  
        <ul>  
          <li>  
            <Link to="/">Foo</Link>  
          </li>  
          <li>  
            <Link to="/bar">Bar</Link>  
          </li>  
          <li>  
            <Link to="/doesnt-exist">Doesn't Exist</Link>  
          </li>  
        </ul> <Switch>  
          <Route exact path="/">  
            <Foo />  
          </Route>  
          <Route path="/bar">  
            <Bar />  
          </Route>  
          <Route path="*">  
            <NotFound />  
          </Route>  
        </Switch>  
      </div>  
    </Router>  
  );  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

In the code above, we have the Switch component that has the following Route:

<Route path="\*">  
    <NotFound />  
</Route>

It’ll match any URL that doesn’t match the other routes.

Therefore, when we click the Doesn’t Exist link, we’ll see Not Found.

This also applies when we type in anything other / or /bar as the relative path.

The * is the wildcard character for matching anything.

Recursive Routes

Just like any other routes, we can define routes recursively. The only difference is that we reference the component within the Route with itself.

For example, if we have an array of NEIGHBORS as follows:

const NEIGHBORS = [  
  { id: 0, name: "Jane", neighbors: [1, 2, 3] },  
  { id: 1, name: "Joe", neighbors: [0, 3] },  
  { id: 2, name: "Mary", neighbors: [0, 1, 3] },  
  { id: 3, name: "David", neighbors: [1, 2] }  
];

where the neighbors array references the id of the other entries.

Then we can define a Neighbor component and use it as follows:

import React from "react";  
import ReactDOM from "react-dom";  
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";  
import { useParams, useRouteMatch, Redirect } from "react-router";

const NEIGHBORS = [  
  { id: 0, name: "Jane", neighbors: [1, 2, 3] },  
  { id: 1, name: "Joe", neighbors: [0, 3] },  
  { id: 2, name: "Mary", neighbors: [0, 1, 3] },  
  { id: 3, name: "David", neighbors: [1, 2] }  
];

const find = id => {  
  return NEIGHBORS.find(p => p.id === id);  
};

function Neighbor() {  
  const { url } = useRouteMatch();  
  const { id } = useParams();  
  const neighbor = find(+id); return (  
    <div>  
      <h3>{neighbor.name}’s neighbors</h3> <ul>  
        {neighbor.neighbors.map(id => (  
          <li key={id}>  
            <Link to={`${url}/${id}`}>{find(id).name}</Link>  
          </li>  
        ))}  
      </ul> 
      <Switch>  
        <Route path={`${url}/:id`}>  
          <Neighbor />  
        </Route>  
      </Switch>  
    </div>  
  );  
}

function App() {  
  return (  
    <Router>  
      <Switch>  
        <Route path="/:id">  
          <Neighbor />  
        </Route>  
        <Route path="/">  
          <Redirect to="/0" />  
        </Route>  
      </Switch>  
    </Router>  
  );  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

In the code above, we have the find function to make find neighbors easy.

Then in the Neighbor component, we have:

const { id } = useParams();  
const neighbor = find(+id);

to get the id from the URL, and then find the neighbor with it using the find function.

Then we display the neighbors of the neighbor by writing:

<ul>  
    {neighbor.neighbors.map(id => (  
    <li key={id}>  
        <Link to={`${url}/${id}`}>{find(id).name}</Link>  
    </li>  
    ))}  
</ul>

Then we have a Switch component that has Neighbor again in the Route :

<Switch>  
    <Route path={`${url}/:id`}>  
        <Neighbor />  
    </Route>  
</Switch>

This is the part that’s recursive, since Neighbor references itself here. We set the path to `${url}/:id` so that when the Link s above is clicked, we’ll see the new neighbors.

Then in App , we just have the usual routes. Once again, we have path=”/:id” to find a neighbor by id .

Conclusion

We can define 404 routes easily by setting the path ‘ value of the Route to an asterisk.

To define recursive routes, we can reference routes within itself. It’s treated no differently than any other kind of route.

Categories
JavaScript React

Lazy Load Your React Code With Code-Splitting

React is a library for creating front-end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

In this article, we’ll look at how to make apps load faster by splitting code so that only the parts that are needed will load.


Code-Splitting

We need code-splitting so that product React bundles won’t be too big. As our apps get bigger, the production bundles will also get bigger and take longer to load if we don’t split them and load them only when needed.

Create React App has code-splitting support built-in. We can use the lazy and import functions from React to achieve this.

For example, we can use those functions as follows:

Foo.js:

import React from "react";export default function Foo() {  
  return <div>foo</div>;  
}

App.js:

In the code above, we have Foo.js with the Foo component.

Then, in App.js, we have the App component that has Suspense to load the fallback. UI has the Foo import loads. We need this since Foo is loaded at runtime.

React.lazy loads the Foo component at runtime instead of at build time. It also splits the code from the bundle into a separate file so that each bundled file is smaller.

We’ll get an error telling us to add a fallback UI with the Suspense component if it’s missing.

The fallback prop accepts any React element that we want to render while waiting for the component to load. We can place Suspense anywhere above the lazy component.

Wrapping multiple elements in a single Suspense element also works.


Error Boundaries

To fail gracefully when other modules fail to load, such as where network failure occurs, we can use error boundary components.

To use them, we wrap them around the Suspense component as follows:

ErrorBoundary.js:

Foo.js:

import React from "react";

export default function Foo() {  
  return <div>foo</div>;  
}

App.js:

In the code above, we added the ErrorBoundary component in ErrorBoundary.js.

Then, in App, we wrapped it around our Suspense component, which lets us catch any errors that occur when loading the Foo component.


Route-Based Code-Splitting

Many React apps are single-page apps. They’ll have routes to map URLs to components.

We can split code based on routes. For example, we can incorporate React Router routes into our app and split code based on routes as follows:

Foo.js:

import React from "react";

export default function Foo() {  
  return <div>foo</div>;  
}

Bar.js:

import React from "react";

export default function Bar() {  
  return <div>bar</div>;  
}

App.js:

In the example above, we add the Router component around all our components.

We put the Routes inside the Suspense component so that they can be lazy-loaded. That is, they load only when we load the route in our browser.

This is also why we need the Suspense component around the routes. We need the fallback UI so that it’ll be shown when the routes are loading.


Named Exports

React.lazy only supports default exports. If our module uses named exports, then we have to create an intermediate module that re-exports it as default.

For example, we can arrange our code as follows:

Foo.js:

import React from "react";

export const Foo = function() {  
  return <div>foo</div>;  
};

FooDefault.js:

import { Foo } from "./Foo";

export default Foo;

App.js:

import React, { Suspense } from "react";
const FooComponent = React.lazy(() => import("./Foo"));
export default function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <FooComponent />
    </Suspense>
  );
}

In the code above, we have FooDefault.js that exports Foo as a default export.

Then, we imported it in App.js with React.lazy.


Conclusion

Code-splitting is easy with Create React App and React. React has the lazy method to import components on the fly.

We need a fallback UI so that we can see something when the dynamically imported component loads.

Error boundaries are also supported and we can use it to gracefully fail when a dynamically imported component fails to load.

Code-splitting is also supported by React Router. React.lazy is smart enough to split code by route.

Categories
JavaScript React

Creating Sidebars with React Router

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

To build single-page apps, we have to have some way to map URLs to the React component to display.

In this article, we’ll look at how to create sidebars with route content only displaying in a portion of the screen with React Router.

Creating Sidebar

We can easily create a fixed sidebar on one side of the screen while displaying route content on the other.

To do this, we just need to add our sidebar on one side with links and the Switch and Route components on the other.

For example, we can write the following code to achieve this effect:

import React from "react";  
import ReactDOM from "react-dom";  
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

const routes = [  
  {  
    path: "/",  
    exact: true,  
    sidebar: () => <div>home</div>,  
    main: () => <h2>Home</h2>  
  },  
  {  
    path: "/foo",  
    sidebar: () => <div>foo</div>,  
    main: () => <h2>Bubblegum</h2>  
  },  
  {  
    path: "/shoelbaraces",  
    sidebar: () => <div>bar</div>,  
    main: () => <h2>Shoelaces</h2>  
  }  
];

function App() {  
  return (  
    <Router>  
      <div style={{ display: "flex" }}>  
        <div  
          style={{  
            padding: "10px",  
            width: "40%",  
            background: "pink"  
          }}  
        >  
          <ul style={{ listStyleType: "none", padding: 0 }}>  
            <li>  
              <Link to="/">Home</Link>  
            </li>  
            <li>  
              <Link to="/foo">Foo</Link>  
            </li>  
            <li>  
              <Link to="/bar">Bar</Link>  
            </li>  
          </ul> <Switch>  
            {routes.map((route, index) => (  
              <Route  
                key={index}  
                path={route.path}  
                exact={route.exact}  
                children={<route.sidebar />}  
              />  
            ))}  
          </Switch>  
        </div> <div style={{ flex: 1, padding: "10px" }}>  
          <Switch>  
            {routes.map((route, index) => (  
              <Route  
                key={index}  
                path={route.path}  
                exact={route.exact}  
                children={<route.main />}  
              />  
            ))}  
          </Switch>  
        </div>  
      </div>  
    </Router>  
  );  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<App />, rootElement);

In the code above, we have the sidebar in the top of App . And we have the routes array as follows:

const routes = [  
  {  
    path: "/",  
    exact: true,  
    sidebar: () => <div>home</div>,  
    main: () => <h2>Home</h2>  
  },  
  {  
    path: "/foo",  
    sidebar: () => <div>foo</div>,  
    main: () => <h2>Bubblegum</h2>  
  },  
  {  
    path: "/shoelbaraces",  
    sidebar: () => <div>bar</div>,  
    main: () => <h2>Shoelaces</h2>  
  }  
];

In the outermost div of App , we have display: 'flex' style to display the sidebar and the route content side by side, with the sidebar of the left and the route content on the right.

The sidebar is composed of the following code in App :

<div style={{ padding: "10px", width: "40%", background: "#f0f0f0" }}>  
    <ul style={{ listStyleType: "none", padding: 0 }}>  
        <li>  
            <Link to="/">Home</Link>  
        </li>  
        <li>  
            <Link to="/foo">Foo</Link>  
        </li>  
        <li>  
            <Link to="/bar">Bar</Link>  
        </li>  
    </ul>  
    <Switch>  
        {routes.map((route, index) => (  
        <Route key={index} path={route.path} exact={route.exact} children={<route.sidebar />} /> ))}  
    </Switch>  
</div>

In the code above, we have the ul element to display the Link s, which we can click on the show the route that we want.

In the Switch component, we have the routes array mapped to Route s that we want to display in the sidebar.

We want to display the component we set as the value of the sidebar in each entry of routes . Therefore, we set children to that.

In the routes array, we have the route with path set to '/' has the exact property set to true .

We have to do this so that React Router won’t direct all routes that start with a / to the ‘home’ route. This is because React Router doesn’t look at the other routes once it found a match.

If the match isn’t exact, then it’ll take anything that starts with a / as the correct match. In this case, it’ll be the first route.

The right side is composed of the following code:

<div style={{ flex: 1, padding: "10px" }}>  
    <Switch>  
        {routes.map((route, index) => (  
        <Route key={index} path={route.path} exact={route.exact} children={<route.main />} /> ))}  
    </Switch>  
</div>

The code above maps the routes array again to Route s, but we set the children prop to the components set to the main property instead of sidebar .

Conclusion

We can create a sidebar easily by using flexbox and the Switch and Route components.

As long as we have Switch and Route components in only one portion of the screen, the route content will only be displayed on that part of the screen.

Flexbox lets us display items side by side without hassle.

Categories
JavaScript Vue

Vue Router 4 — Navigation

Vue Router 4 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Navigation

We can navigate to a route with the this.$router ‘s methods.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
        <a @click="goBack" href="#">Go Back</a>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = { template: "<div>foo</div>" };
      const Bar = { template: "<div>bar</div>" };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/bar", component: Bar }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        methods: {
          goBack() {
            window.history.length > 1
              ? this.$router.go(-1)
              : this.$router.push("/foo");
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

to create a goBack method and call it when we click on the Go Back link.

In the method, we check if there are anything in the browser history.

If there is, then we go back to the previous page.

Otherwise, we go to the /foo page.

Reacting to Params Changes

We can react to route params changes in a few ways.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        watch: {
          $route(to, from) {
            console.log(to, from);
          }
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have the /foo/:id route that maps to the Foo component.

And in the Foo component, we have the $route watcher to watch the route.

to has the route to go to. from has the route that we departed from.

They’re both objects with the path, route metadata, route parameters, query parameters, and more.

Also, we can use the beforeRouteUpdate method to watch for route changes:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="[https://unpkg.com/vue@next](https://unpkg.com/vue@next)"></script>
    <script src="[https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js](https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js)"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        beforeRouteUpdate(to, from, next) {
          console.log(to, from);
          next();
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

Instead of watching the $route object, we have the beforeRouteUpdate hook, which is made available with the app.use(router) call.

Now when we click on the links, we’ll see the same data as we did with the watcher.

The only difference is that we call next to move to the next route.

Conclusion

We can watch for navigation with watchers or hooks with Vue Router 4.