vue-router
vue-router copied to clipboard
[Feature Request] Add support for Vue 2.7
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
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 Should we change source code to typescript align to vue 2.7?
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
Ok, can I take this? I'm willing to create a PR about vue 2.7.
Of course!
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/
.
@psalaets much expect!
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()
// ...
}
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 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 watch
ed.
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.
@ElteHupkes Indeed, I modified the previous method, but this is only a temporary solution
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.
Inspired by source code of v4, I write the composable like this:
Effect:
- get initial data in setup (
useRoute
anduseRouter
) - 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 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 onreactive(route)
, unless I removed thematches
property. I didn't analyze this fully (I was about done with this mess to be honest, and I haven't neededmatches
so far), but I suspect that my<script setup />
components were exposing theroute
object when importing it, and since those components were inmatches
, an infinite cycle was created. So if you see anything like that, that would be something to look for.
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
Hi! When will this minor version be released? @posva
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 👍
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)
})
For what it's worth, these solutions aren't sufficient if you're trying to use new APIs such as addRoute
, hasRoute
, or removeRoute
.
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
}
]
}
]
Any chance to get NavigationGuards to work with script setup in Vue 2.7?
@posva this is great, is there any chance we can get removeRoute
and hasRoute
added to this version's API?
@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.
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.
It works ok now with version 3.6.4, ¡Gracias!
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.
@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?
I am curious why use shallowReactive instead of reactive?
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)