Reactive Vue Routes With the Composition API

Reactive Vue Routes With the Composition API

  • 633

The Vue.js Composition API is a set of function-based APIs that allows you to make a flexible composition of component logic. This is really useful to write reusable logic and to make code well organized.

The Vue.js Composition API is a set of function-based APIs that allows you to make a flexible composition of component logic. This is really useful to write reusable logic and to make code well organized.

This article will introduce how to directly watch the route object in a Vue project.

Here’s the final codebase in GitHub: manakuro/reactive-vue-route-in-composition-api-example.

Set Up Nuxt Project

To quickly start off, we will create a Nuxt project:

npx create-nuxt-app my-app

After installation, you can start the dev server by running the command:

yarn dev

Set Up Composition API

Install the Composition API:

yarn add @vue/composition-api

Create a plugins/composition-api.js:

import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

Add it to plugins in nuxt.config.js:

plugins: ['~/plugins/composition-api'],

Add Set-Up Function

Now that we can use the Composition API in SFC, let’s make use of the feature.

Let’s suppose that we have a button that changes query params in URL, like this:

<template>
  <div class="container">
    <div>
      <logo />
      <h2 class="subtitle">Reactive Router query: {{ myQuery }}</h2>
      <div class="links">
        <a
          class="button--grey"
          @click="handleClick"
        >
          Change Router Query
        </a>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent } from '@vue/composition-api'
import Logo from '~/components/Logo.vue'
export default defineComponent({
  components: {
    Logo
  },
  setup(_, ctx) {
    const route = ctx.root.$route
    const myQuery = computed(() => route.query.myQuery)
    const handleClick = (e: Event) => {
      e.preventDefault()
      ctx.root.$router.push({ query: { myQuery: 'handle my query!' } })
    }
    return {
      myQuery,
      handleClick
    }
  }
})
</script>

index.vue

When you want to use the route in the set-up function, you can access it through the ctx, which includes the Vue instance object.

Let’s see how it works:

This is image title

I expected the text to be changed to React Router query: handle my query! after clicking the button. But it didn’t change because the route object is just a plain object, not a reactive one.

To make it work, we need to make the route reactive by passing a reference.

Change the code to this:

<script lang="ts">
import {
  computed,
  defineComponent,
  reactive,
  watch
} from '@vue/composition-api'
import Logo from '~/components/Logo.vue'
export default defineComponent({
  components: {
    Logo
  },
  setup(_, ctx) {
    const route = ctx.root.$route
    const state = reactive({ route })
    watch(
      () => ctx.root.$route,
      (r) => {
        state.route = r as any
      }
    )
    const myQuery = computed(() => state.route.query.myQuery)
    const handleClick = (e: Event) => {
      e.preventDefault()
      ctx.root.$router.push({ query: { myQuery: 'handle my query!' } })
    }
    return {
      myQuery,
      handleClick
    }
  }
})
</script>

index.vue

We’ve added a reactive state, including the route object. The reactive function equals Vue.observable, which makes an object reactive. Internally, Vue uses this on the object returned by the data function.

We’ve made $route watchable by using the watch function because we need to update the state.route object with a new one after $router has changed something.

Let’s see how it works:
This is image title

Good. It works well.

Now that we’ve made it, we want to make the route more reusable across the project.

To do that, we will extract the code and create useRouter as a Hook function.

Let’s create hooks/useRouter.ts:

import { reactive, toRefs, watch } from '@vue/composition-api'
import { Route } from 'vue-router'
import { getRuntimeVM } from '~/utils/runtime'

type State = {
  route: Route
}

const useRouter = () => {
  const vm = getRuntimeVM()
  const state = reactive<State>({
    route: vm.$route
  })

  watch(
    () => vm.$route,
    (r) => {
      state.route = r as any
    }
  )

  return { ...toRefs(state), router: vm.$router }
}

useRouter.ts

toRefs is useful when returning a reactive object from a composition function to destructure the returned object without losing reactivity.

Then we edit the Vue file:

<script lang="ts">
import { computed, defineComponent } from '@vue/composition-api'
import Logo from '~/components/Logo.vue'
import useRouter from '~/hooks/useRouter'
export default defineComponent({
  components: {
    Logo
  },
  setup() {
    const { route, router } = useRouter()
    const myQuery = computed(() => route.value.query.myQuery)
    const handleClick = (e: Event) => {
      e.preventDefault()
      router.push({ query: { myQuery: 'handle my query!' } })
    }
    return {
      myQuery,
      handleClick
    }
  }
})
</script>

index.vue

Good! This is very declarative and makes it a more reusable function.

Conclusion

We’ve covered how to watch the route object with the Composition API. The key idea of the Composition API is to make code organized, readable, and reusable. If you find yourself writing a bunch of code in the set-up function, just try to separate it by concerns and create a useXXX function to reuse it. You can see some examples of code on GitHub.