vue-router icon indicating copy to clipboard operation
vue-router copied to clipboard

[Feature Request] Add support for Vue 2.7

Open kingyue737 opened this issue 2 years ago • 21 comments

What problem does this feature solve?

As composition api has been incoporated in vue 2.7, vue-router should provide useRoute and useRouter to enable router usage in <setup script>.

What does the proposed API look like?

const router = useRouter()
router.push(...)

Just like in Vue3

kingyue737 avatar Jun 20 '22 08:06 kingyue737

I think it makes sense to release this as a new minor. It would also be the last minor we release in Vue Router to align with Vue Core.

posva avatar Jun 20 '22 08:06 posva

@posva Should we change source code to typescript align to vue 2.7?

xiaoxiangmoe avatar Jul 04 '22 13:07 xiaoxiangmoe

no, because this codebase is not going to get the upgrade vue (adding a whole set of new APIs).

Vue Router v4 composables should be exposed as a sub import: import {} from 'vue-router/composables' to avoid any breakage

posva avatar Jul 04 '22 13:07 posva

Ok, can I take this? I'm willing to create a PR about vue 2.7.

xiaoxiangmoe avatar Jul 04 '22 13:07 xiaoxiangmoe

Of course!

posva avatar Jul 04 '22 13:07 posva

I made a 2.7 compatible version. https://github.com/logue/vue2-helpers

Use it as follows:

import { useRouter } from '@logue/vue2-helpers/vue-router';

When officially supported, you can use it as it is by deleting @logue/vue2-helpers/.

logue avatar Jul 06 '22 08:07 logue

@psalaets much expect!

fyeeme avatar Jul 06 '22 15:07 fyeeme

I made a 2.7 compatible version. https://github.com/logue/vue2-helpers

Use it as follows:

import { useRouter } from '@logue/vue2-helpers/vue-router';

When officially supported, you can use it as it is by deleting @logue/vue2-helpers/.

You can do this!

// utils.js
import { getCurrentInstance } from 'vue'

export function useRoute() {
  const { proxy } = getCurrentInstance()
  const route = proxy.$route
  return route
}
export function useRouter() {
  const { proxy } = getCurrentInstance()
  const router = proxy.$router
  return router
}
import { useRouter, useRoute } from '@/utils/index.js' // '@/utils/index.js' => 'vue-router'

setup() {
  const router = useRouter()
  const route = useRoute()
  // ...
}

0xe69e97 avatar Jul 08 '22 02:07 0xe69e97

You can use it temporarily:

// in router/index
import { computed, reactive } from 'vue'
import VueRouter from 'vue-router'

let Router = null

export function setupRouter(app) {
  app.use(VueRouter)

  Router = new VueRouter({
    mode: 'history',
  })

  // Router === reactive(Router) reactive use Vue.observable
  reactive(Router)

  return Router
}

export function useRoute() {
  const routeEffect = computed(() => Router?.currentRoute || {})
 
  /**
   * or
   */
   return {
      ...(Router?.currentRoute || {}),
      __effect__: routeEffect, //  is ref
    }

  /**
   * or
   */
  // return { route: Router?.currentRoute, routeEffect };

  /**
   * use in setup
   * const { route, routeEffect } = useRoute()
   * console.log(route);       //  { fullPath: '/', name: null, ...}
   * console.log(routeEffect); //  { effect: {...}, value: {...} }
   */

  /**
   * or
   * If you don't mind using .value
   */
  // return routeEffect
}

or vuex:

import Vuex from 'vuex'

const Store = new Vuex.Store({})

export function useStore() {
  return Store
}

and use :

<script setup>
import { computed } from 'vue' 
import { useRouter, useRoute } from '@/router'
import { useStore } from '@/store'

const store = useStore()

const router = useRouter()
const route = useRoute()

const currentRouteName = computed(() =>route.__effect__.value.name)

</script>

Lupeiwen0 avatar Jul 14 '22 09:07 Lupeiwen0

@Lupeiwen0 One of the reasons for wanting to have this is the ability to watch for changes in the current route object (parameters, query parameters, etc) from the active component. router.currentRoute will give you access to the correct route object in setup, but that object can't be watched.

ElteHupkes avatar Jul 14 '22 14:07 ElteHupkes

I just wanted to write this feature, and you guys have already done it.

It's exactly what I thought it would be. Ha ha.

EveChee avatar Jul 15 '22 02:07 EveChee

@ElteHupkes Indeed, I modified the previous method, but this is only a temporary solution

Lupeiwen0 avatar Jul 15 '22 02:07 Lupeiwen0

I took a look at your implementation, and the responsiveness is still a bit of a problem.

So I proposed a PR of my own.

EveChee avatar Jul 15 '22 02:07 EveChee

Inspired by source code of v4, I write the composable like this:

Effect:

  • get initial data in setup ( useRoute and useRouter )
  • route object can be watched in setup ( useRoute )

For Js:

// @/router/useApi.js
import { reactive, shallowRef, computed } from 'vue'

/**
 * @typedef { import("vue-router").default } Router
 * @typedef { import("vue-router").Route } Route
 */

/**
 * vue-router composables
 * @param {Router} router - router instance
 */
export function useApi(router) {
  const currentRoute = shallowRef({
    path: '/',
    name: undefined,
    params: {},
    query: {},
    hash: '',
    fullPath: '/',
    matched: [],
    meta: {},
    redirectedFrom: undefined,
  })

  /** @type {Route} */
  const reactiveRoute = {}
  
  for (const key in currentRoute.value) {
    reactiveRoute[key] = computed(() => currentRoute.value[key])
  }
  
  router.afterEach((to) => {
    currentRoute.value = to
  })
  
  /**
   * get router instance
   */
  function useRouter() {
    return router
  }
  
  /**
   * get current route object
   */
  function useRoute() {
    return reactive(reactiveRoute)
  }

  return {
    useRouter,
    useRoute
  }
}

For Ts:

// @/router/useApi.ts
import { reactive, shallowRef, computed } from 'vue'
import type { default as VueRouter, Route } from 'vue-router'

export function useApi(router: VueRouter) {
  const currentRoute = shallowRef<Route>({
    path: '/',
    name: undefined,
    params: {},
    query: {},
    hash: '',
    fullPath: '/',
    matched: [],
    meta: {},
    redirectedFrom: undefined,
  })
  
  const reactiveRoute = {} as Route
  
  for (const key in currentRoute.value) {
    // @ts-expect-error: the key matches
    reactiveRoute[key] = computed(() => currentRoute.value[key])
  }
  
  router.afterEach((to) => {
    currentRoute.value = to
  })
  
  function useRouter() {
    return router
  }
  
  function useRoute() {
    return reactive(reactiveRoute)
  }

  return {
    useRouter,
    useRoute
  }
}

usage:

// @/router/index.js
import Router from 'vue-router'
import { useApi } from './useApi'

const router = new Router({
  // ...
})
const { useRoute, useRouter } = useApi(router) 

export default router
export { useRoute, useRouter }

Lphal avatar Jul 15 '22 08:07 Lphal

@Lphal Thanks, that looks similar to something I ended up with, and a bit less messy. Two things to pay attention to when using this:

  • router.afterEach() fires before the route / component that is currently active is dismounted, so you need to deal with that watcher potentially firing when you don't want it to anymore. I encountered a scenario where the watcher tried to update some data based on the URL, but that was now the URL of a new route with the expected parameters missing. I honestly don't know if this is also an issue with the same APIs in Vue 3, apart from this StackOverflow issue I haven't been able to find much about it. I also haven't yet found a completely satisfactory workaround. My current workaround involves scheduling the watcher for the next tick and checking if the component is still mounted then.
  • I had issues with useRoute() creating infinite recursion on reactive(route), unless I removed the matches property. I didn't analyze this fully (I was about done with this mess to be honest, and I haven't needed matches so far), but I suspect that my <script setup /> components were exposing the route object when importing it, and since those components were in matches, an infinite cycle was created. So if you see anything like that, that would be something to look for.

ElteHupkes avatar Jul 15 '22 11:07 ElteHupkes

One more problem should be adressed to support vue 2.7 with typescript.

When using component as normal import (not async import), vue-tsc throws error.

App.vue

<template />
<script lang="ts">
import {defineComponent} from "vue";

export default defineComponent({});
</script>

routes.ts

import {RouteConfig} from "vue-router"

import App from "./App.vue";

const routes: RouteConfig[] = [
  {
    path: "/my/orders/",
    name: "OrdersTable",
    component: App,
  },
];

export default routes;
> vue-tsc
src/routes.ts:9:5 - error TS2322: Type 'DefineComponent<{}, unknown, Data, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, Readonly<ExtractPropTypes<{}>>, {}>' is not assignable to type 'Component | undefined'.
  Type 'ComponentPublicInstanceConstructor<Vue3Instance<Data, Readonly<ExtractPropTypes<{}>>, Readonly<ExtractPropTypes<{}>>, {}, {}, true, ComponentOptionsBase<any, any, ... 7 more ..., any>> & ... 5 more ... & Readonly<...>, ... 4 more ..., MethodOptions> & ComponentOptionsBase<...> & { ...; }' is missing the following properties from type 'VueConstructor<Vue<Record<string, any>, Record<string, any>, never, never, never, never, (event: string, ...args: any[]) => Vue<Record<string, any>, Record<string, any>, ... 4 more ..., ...>>>': extend, nextTick, set, delete, and 10 more.

9     component: App,
      ~~~~~~~~~


Found 1 error in src/routes.ts:9

Full example here. https://github.com/last-partizan/vue-examples/tree/97ab9dac381459c859c257ac60bfa1913efc8aba

This could be fixed by adding DefineComponent to router.d.ts

type Component = ComponentOptions<Vue> | typeof Vue | AsyncComponent | DefineComponent

last-partizan avatar Jul 15 '22 14:07 last-partizan

Hi! When will this minor version be released? @posva

maomincoding avatar Jul 18 '22 07:07 maomincoding

I copy/pasted the code from the ongoing PR with success as a temporary workaround in my app ;)

https://github.com/vuejs/vue-router/pull/3763

Thanks everyone 👍

nicooprat avatar Jul 18 '22 12:07 nicooprat

I've been using something that is very similar to @Lphal snippet! Hope it helps, good luck with the migrations folks 🚀 💪

// if using 2.6.x import from @vue/composition-api
import { computed, reactive, getCurrentInstance } from 'vue'

export function useRouter() {
  return getCurrentInstance().proxy.$router
}

export function useRoute() {
  const currentRoute = computed(() => getCurrentInstance().proxy.$route)

  const protoRoute = Object.keys(currentRoute.value).reduce(
    (acc, key) => {
      acc[key] = computed(() => currentRoute.value[key])
      return acc
    },
    {}
  )

  return reactive(protoRoute)
}

Didn't do the most extensive testing but you can cover the basics with this

vue-router-migration-utils.spec.js
import { createLocalVue } from '@vue/test-utils'
import { mountComposable } from '@/spec/helpers/mountComposable.js'
import { useRouter, useRoute } from '@/src/vue3-migration/vue-router.js'
import VueRouter from 'vue-router'
// remove if vue2.7
import VueCompositionAPI from '@vue/composition-api'

const localVue = createLocalVue()
localVue.use(VueRouter)
// remove if vue2.7
localVue.use(VueCompositionAPI)

// [1] https://lmiller1990.github.io/vue-testing-handbook/vue-router.html#writing-the-test
function createRouter() {
  return new VueRouter({
    mode: 'abstract',
    routes: [
      {
        path: '/',
        name: 'index',
        meta: { title: 'vue-router hooks index' }
      },
      {
        path: '/home',
        name: 'home',
        meta: { title: 'vue-router hooks home' }
      },
      {
        path: '*',
        name: '404',
        meta: { title: '404 - Not Found' }
      }
    ]
  })
}

function useVueRouterFactory() {
  // original return values, not unRefd template ones that we get via wrapper.vm
  let router, route
  const wrapper = mountComposable(
    () => {
      router = useRouter()
      route = useRoute()
    },
    { localVue, router: createRouter() }

  )

  return { wrapper, router, route }
}

it('both router and route return values should be defined', () => {
  const { router, route } = useVueRouterFactory()
  expect(router).toBeDefined()
  expect(route).toBeDefined()
})

it('route is reactive to router interaction', async () => {
  const { route, router } = useVueRouterFactory()

  expect(route.name).toBe(null) // see [1]

  await router.push('/home')
  expect(route.name).toBe('home')
  expect(route.meta.title).toBe('vue-router hooks home')
  expect(router.currentRoute.name).toBe('home')

  await router.push('/route-that-does-not-exist')
  expect(route.name).toBe('404')
  expect(route.meta.title).toBe('404 - Not Found')
  expect(router.currentRoute.name).toBe('404')
})

it('accepts global router guards callbacks', async () => {
  const { router } = useVueRouterFactory()
  const onAfterEach = jest.fn()

  router.afterEach(onAfterEach)
  expect(router.afterHooks.length).toBe(1)

  await router.push('/')
  expect(onAfterEach).toHaveBeenCalledTimes(1)
})

// meta testing, ensure that we're not leaking currentRoute to other assertions
// of this suite. see [1] if you remove "mode: abstract" this will fail.
// because route will be the last one from previous spec
it('it cleanups vue router afterwards between specs', () => {
  const { route } = useVueRouterFactory()

  expect(route.name).toBe(null)
})

renatodeleao avatar Jul 21 '22 17:07 renatodeleao

For what it's worth, these solutions aren't sufficient if you're trying to use new APIs such as addRoute, hasRoute, or removeRoute.

lwansbrough avatar Jul 26 '22 21:07 lwansbrough

as a temporary fix for the issue with the DefineComponent type as described by @last-partizan, you can cast the component to any in the route config. This is good enough to get TypeScript to not complain until vue-router has more official support

const routes = RouteConfig[] = [
  {
    path: '/',
    component: SomeComponent // oddly, I don't have this type error on parent routes...only the children
    children: [
      {
        path: 'something',
        component: AnotherComponent as any, // the workaround
      }
    ] 
  }
]

cinderisles avatar Aug 05 '22 17:08 cinderisles

Any chance to get NavigationGuards to work with script setup in Vue 2.7?

DerAlbertCom avatar Aug 12 '22 20:08 DerAlbertCom

@posva this is great, is there any chance we can get removeRoute and hasRoute added to this version's API?

lwansbrough avatar Aug 22 '22 17:08 lwansbrough

@posva Thanks for the update!

I've been trying to make it work with this configuration:

  • vue 2.7.10
  • vue-router 3.6.3
  • vue-cli 4.5.19 (Webpack 4)

But getting some error messages in the build (see this thread)

According to @YfengFly, probably it is a problem with webpack 4 (used by vue-cli 4).

For the moment, I've managed to solve it using

import { useRoute } from "vue-router/dist/composables"

Not the most elegant way to do it, but it works. It would be interesting to adapt it so it woks cleanly as originally intended by importing from "vue-router/composables" using webpack 4.

vate avatar Aug 24 '22 13:08 vate

There are some issues with Webpack 4 since it doesn't support the exports field. @xiaoxiangmoe helped me out on this one and a fix should be released soon.

posva avatar Aug 25 '22 06:08 posva

It works ok now with version 3.6.4, ¡Gracias!

vate avatar Aug 25 '22 09:08 vate

This is driving me nuts. Using useRoute in App.vue and a comptued property like

 const route = useRoute();
   const isPublicPage = computed(() => {
     return (
       route.name === "currentSelectedSchoolList" ||
       route.name === "publicLineupForm"
     );
   });
correctly renders after the component loads , but within setup route is not correctly loaded.
As a result `isPublicPage.value` is incorrect.

rachitpant avatar Apr 10 '23 15:04 rachitpant

@posva My project has not been upgraded to vue2.7, but I have used the library @vue/composition-api. I wonder if it will work if I change the import source from vue to @vue/composition-api via Babel

// vue-router/dist/composables.mjs
import { getCurrentInstance, effectScope, shallowReactive, onUnmounted, computed, unref } from 'vue';

// transform by Babel
import { getCurrentInstance, effectScope, shallowReactive, onUnmounted, computed, unref } from '@vue/composition-api';

is there any risk?

twt898xu avatar Apr 21 '23 01:04 twt898xu

I am curious why use shallowReactive instead of reactive?

dews avatar May 16 '23 05:05 dews

This is currently not working with:

  • vue 2.7.16
  • vue-router 3.6.5

Usage via composition API:

import { useRoute, useRouter } from "vue-router/composables";

getCurrentInstance returns empty and throws an error:

Error: [vue-router]: Missing current instance. useRouter() must be called inside <script setup> or setup().
    at throwNoCurrentInstance (composables.mjs:22:11)
    at useRouter (composables.mjs:30:5)
    at setup (FooComponent.tsx:50:33)
    at mergedSetupFn (vue-composition-api.mjs:2221:113)
    at eval (vue-composition-api.mjs:2035:23)
    at activateCurrentInstance (vue-composition-api.mjs:1954:16)
    at initSetup (vue-composition-api.mjs:2033:9)
    at VueComponent.wrappedData (vue-composition-api.mjs:2016:13)
    at getData (vue.js:4443:23)
    at initData (vue.js:4409:44)

woahitsjc avatar Feb 16 '24 12:02 woahitsjc