Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Add support for Vue 2.7 #3760

Closed
kingyue737 opened this issue Jun 20, 2022 · 32 comments · Fixed by #3769
Closed

[Feature Request] Add support for Vue 2.7 #3760

kingyue737 opened this issue Jun 20, 2022 · 32 comments · Fixed by #3769

Comments

@kingyue737
Copy link

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

@posva
Copy link
Member

posva commented Jun 20, 2022

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.

@xiaoxiangmoe
Copy link
Contributor

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

@posva
Copy link
Member

posva commented Jul 4, 2022

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

@xiaoxiangmoe
Copy link
Contributor

xiaoxiangmoe commented Jul 4, 2022

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

@posva
Copy link
Member

posva commented Jul 4, 2022

Of course!

@logue
Copy link

logue commented Jul 6, 2022

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/.

@fyeeme
Copy link

fyeeme commented Jul 6, 2022

@psalaets much expect!

@0xe69e97
Copy link

0xe69e97 commented Jul 8, 2022

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()
  // ...
}

@Lupeiwen0
Copy link

Lupeiwen0 commented Jul 14, 2022

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>

@ElteHupkes
Copy link

ElteHupkes commented Jul 14, 2022

@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.

@EveChee
Copy link

EveChee commented Jul 15, 2022

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.

@Lupeiwen0
Copy link

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

@EveChee
Copy link

EveChee commented Jul 15, 2022

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.

@Lphal
Copy link

Lphal commented Jul 15, 2022

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 }

@ElteHupkes
Copy link

@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.

@last-partizan
Copy link

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

@maomincoding
Copy link

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

@nicooprat
Copy link

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

#3763

Thanks everyone 👍

@renatodeleao
Copy link

renatodeleao commented Jul 21, 2022

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)
})

@lwansbrough
Copy link

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

@cinderisles
Copy link

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
      }
    ] 
  }
]

@DerAlbertCom
Copy link

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

@lwansbrough
Copy link

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

@vate
Copy link

vate commented Aug 24, 2022

@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.

@posva
Copy link
Member

posva commented Aug 25, 2022

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.

@vate
Copy link

vate commented Aug 25, 2022

It works ok now with version 3.6.4, ¡Gracias!

@rachitpant
Copy link

rachitpant commented Apr 10, 2023

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.

@twt898xu
Copy link

@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?

@dews
Copy link

dews commented May 16, 2023

I am curious why use shallowReactive instead of reactive?

@woahitsjc
Copy link

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)

@dennybiasiolli
Copy link

@woahitsjc do you have an example repo for this?
I'm using your exact code and it works.
Make sure you are using <script setup> or inside a setup() function.

@woahitsjc
Copy link

@dennybiasiolli I tried to setup an example repo but couldn't replicate the issue.

Although, I managed to find the cause of the problem. It was using a package called @vueblocks/vue-use-vuex to grab the vuex store with the method useStore which was causing the vue instance to return null.

To get the store now, I'm doing:

const instance = getCurrentInstance();
const store = instance.proxy.$store;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.