rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

How to use vue-router (this.$router/$route) in the new function-based API?

Open beeplin opened this issue 6 years ago • 32 comments

After Vue.use(VueRouter) should route and router be injected into context in setup(props, context)?

beeplin avatar Jul 03 '19 06:07 beeplin

@beeplin Good question. I've been wondering whether, when using this new API, it is preferable to use everything as a hooks:

const { route, router } from useRouter()
console.log(route.params.id)
const goHome = () => {
  router.push({ name: 'home' })
}

I assume Vue.use would still be needed to install components like router-view. But if route and router need to be made available on context, I'm curious what TypeScript implications that would have.

aztalbot avatar Jul 03 '19 07:07 aztalbot

I don't know if there will be a way to inject things to context but the router can be directly imported and it gives access to currentRoute (which is currently not public API but could be eventually exposed as public)

posva avatar Jul 03 '19 07:07 posva

Currently in vue-function-api we can get $router and $route from context.root:

setup(props, context) {
  const path = computed(() => context.root.$route.path)
  ...
}

And I agree it would be better to have something like useRoute.

beeplin avatar Jul 03 '19 07:07 beeplin

I'm with @aztalbot I think.

We can provide functions to provide the router as well as inject it in components where needed:

import { provideRouter, useRouter } from 'vue-router'
import router from './router'

new Vue({
  setup() {
    provideRouter(router)    
  }
})

// ... in a child component
export default {
  setup() {
    const { route /*, router */ } = useRouter()
    const isActive = computed(() => route.name === 'myAwesomeRoute')
    
    return {
      isActive
    }
  }
}
  1. providing the router to the app is a bit more verbose, but also a bit less magical and more explicit?
  2. We can use the route in setup without having to forcing it through the prototype as a $route property on each component
  3. We can even choose to keep it in setup only and not expose it if we only are interested in derrived values (see example above)
  4. we can rename the route object easily before exposing is to the template if we want to etc. (usual "hooks" API advantages)
  5. No more global namespace "pollution" like $route and $router necessary

LinusBorg avatar Jul 03 '19 08:07 LinusBorg

Alternatively to 1., we could have the router object expose the "hook":

import router from './router'

new Vue({
  setup() {
    router.use()
  }
})

LinusBorg avatar Jul 03 '19 09:07 LinusBorg

This will "break" every existing plugin I think, since you cannot access "this.$plugin" directly anymore.

They'll all have to update themselves to use the new API and I don't know how exactly that would work after you install it with options. You can't just import them from the lib right?

phiter avatar Jul 03 '19 14:07 phiter

This will "break" every existing plugin I think, since you cannot access "this.$plugin" directly anymore.

It will only "break" insofar as as plugins that don't yet provide function for use in setup can't be used in setup() and instead have to be used in the current way (object API), so it's not a breaking change in the semver sense.

Since Vue 3 is a major release that comes with other breaking changes, many plugins will have to update either way to stay compatible, and for others, upgrading to work with setup as well seems like a logical step.

Getting back from "plugins" as a whole to the router in particular, I feel that the Vue 3 release would be a good moment to update the API and get rid of the prototype properties that we don't consider ideal anymore, especially since we will have support for provide/inject from the get-go in Vue 3 (it was intorduced post 2.0 in the current major), and migration seems to be straightforward / could be automated with codemods.

I don't know how exactly that would work after you install it with options. You can't just import them from the lib right?

I'm not sure what you are referring to here.

LinusBorg avatar Jul 03 '19 14:07 LinusBorg

I could imagine something like this:

// App.vue
export default {
  setup() {
    const { route, router } = initRouter({ routes, ...otherOptions });
  },
}

// MyComponent.vue
export default {
  setup() {
    const { route, router } = inject(ROUTER_SERVICE);

    // or
    const { route, router } = useRouter();
  },
}

// router.ts
export const ROUTER_SERVICE: Key<RouterService> = Symbol();

const DEFAULT_OPTIONS = {
   serviceKey: ROUTER_SERVICE,
}

export function initRouter(options) {
  options = mergeOptions(options, DEFAULT_OPTIONS);

  const router = new Router(options);
  const route = router.currentRoute;
  const service = { router, route };

  provide(options.serviceKey, service);

  return service;
}

export function useRouter(serviceKey = ROUTER_SERVICE) {
  return inject(serviceKey);
}

backbone87 avatar Jul 03 '19 15:07 backbone87

I'm not sure what you are referring to here.

I was referring to plugins that add options to the instance, like Vue SweetAlert. You can use this.$swal in the component.

With the new api, those plugins would have to allow you to do import { swal } from 'vue-swal'. But that wouldn't load the plugin options, unless it somehow recognizes the options you pass when you init it using Vue.use(plugin).

phiter avatar Jul 03 '19 16:07 phiter

@phiter Well, Vue 3 will have a plugin API as well. Following the proposal #29, mounting an app will work a wee bit differently, but we still have a .use() method and we still have, i.e. a global .mixin() method.

So let's compare before/after:

A plugin like SweetAlert would usually have an install function like this in vue 2:

export default function install ( Vue, options) {
const swal = doSpoemthingWithOptions(options)
Vue.prototype.$swal = swal
}

This is nice and short but has the not so optimal consequence that the prototype gets littered with properties that people try to namespace with $name conventions.

In Vue 3, this would work something like this, better ideas notwithstanding:

import { provide, inject } from 'vue'
const key = new Symbol('swal')
export default function install (app, options) {
  const swal = doSomethingWithOptions(options)

  // using a global mixins here to  add a global setup().
  // maybe we can have an `app.setup()` shortcut? 
  // #29 was written long before we came up with that new API
  app.mixin({ 
    setup() {
      provide(key, swal)
    }
  })
}

export function useSwal() {
  return inject(key)
}

Usage:

// setup
import { createApp } from 'vue'
import App from './App.vue'
import VueSweetAlert from 'vue-sweet-alert'

const app = createApp(App)

app.use(VueSweetAlert, { /* some options */})
app.mount('#app')
// in component:
import { useSwal } from 'vue-sweet-alert'
export default {
  setup() {
    return {
      swal: useSwal()
    }
  }
}

Now, this may seem a bit more verbose, and it is, but it also is more explicit as well as easier to type in TS, it uses Vue's dependency injection (provide/inject) and therefore leaves the prototype of Vue clean and untouched.

Now you still might feel that you do want to inject this into every component because you use it so regularly.

In that case, you could do this instead:

app.mixin({
  setup() {
    return {
      $swal: useSwal()
    }
  }
})

and last but not least don't forget that extending the prototype is still possible. you just won't be able to access those properties from within setup() as things currently stand.

LinusBorg avatar Jul 03 '19 18:07 LinusBorg

My current approach (to avoid disrupting the codebase too much) is to have the standard "2.x" router setup and then use this:

export function useRouter(context: Context) {
  const router = (<any>context.root).$router as VueRouter;
  const copyRoute = (r: Route) => ({
    name: r.name,
    path: r.path,
    fullPath: r.fullPath,
    params: cloneDeep(r.params),
  });
  const route = state(copyRoute(router.currentRoute));
  watch(
    () => {
      return (<any>context.root).$route;
    },
    r => {
      Object.assign(route, copyRoute(r));
    },
  );
  return {
    router,
    route,
  };
}

I use it in my setups like so:

setup(props, context) {
   const { router, route } = useRouter(context);

   const isHome = computed(() => route.name === 'home');

   return { isHome };
}

The cloneDeep and watch thing is so that I can compute stuff off the route which it wasn't working for me by just returning the $route.

thenikso avatar Aug 02 '19 07:08 thenikso

The cloneDeep and watch thing is so that I can compute stuff off the route which it wasn't working for me by just returning the $route.

By using a value() instead of state you don't need the copy.

export function useRouter(context: Context) {
  const router = (<any>context.root).$router as VueRouter;
  const route = value(router.currentRoute);
  watch(
    () => {
      return (<any>context.root).$route;
    },
    r => {
      route.value = r
    },
  );
  return {
    router,
    route,
  };
}

LinusBorg avatar Aug 02 '19 07:08 LinusBorg

By using a value() instead of state you don't need the copy.

I tried that @LinusBorg but I get a strange error:

Cannot assign to read only property 'meta' of object '#<Object>'

I believe it's the Route interaction with some current internals of https://github.com/vuejs/vue-function-api

Also a user would then have to use route.value.name instead of just route.name.

An aside, for the solutions using provide, the current "2.x" plugin will only consider the last provide in the setup so multiple provide do not work as expected.

(perhaps all of this should go in the plugin repo instead of here)

thenikso avatar Aug 02 '19 07:08 thenikso

(perhaps all of this should go in the plugin repo instead of here)

probably

LinusBorg avatar Aug 02 '19 08:08 LinusBorg

Hello, I used the inject/provide example from the RFC, but I had a reactivity issue when I wanted to watch or compute from router.currentRoute... So I provided a reactive router instead of the router instance as is. And it solved my problem:

import Vue from 'vue'
import VueRouter from 'vue-router'
import { provide, inject, reactive } from '@vue/composition-api'

Vue.use(VueRouter)

const router = new VueRouter({ /* ... */ })
  
const RouterSymbol = Symbol()

export function provideRouter() {
  provide(RouterSymbol, reactive(router))
}

export function useRouter() {
  const router = inject(RouterSymbol)
  if (!router) {
    // throw error, no store provided
  }
  return router as VueRouter
}

If it can be of some help for anyone...

plmercereau avatar Jan 10 '20 21:01 plmercereau

I tried, and it work fine

// composables/use-router.js
import { provide, inject } from '@vue/composition-api'

const RouterSymbol = Symbol()

export function provideRouter(router) {
  provide(RouterSymbol, router)
}

export default function useRouter() {
  const router = inject(RouterSymbol)
  if (!router) {
    // throw error, no store provided
  }

  return router
}
------------------
// App.vue
  setup(props, { root: { $router} }) {
    provideRouter($router)
------------
// Usage in component
import useRouter from './user-router'

export default () => {
  const router = useRouter()
......

But don't work with $route (Do the same this, but change context $router -> $route) Could help me?

thearabbit avatar Feb 06 '20 14:02 thearabbit

You can't use inject in a function component if I remember correctly. You have to use an actual component.

LinusBorg avatar Feb 06 '20 18:02 LinusBorg

It work for $router (.push()....), but don't work with $route (.params, ....)

thearabbit avatar Feb 06 '20 23:02 thearabbit

That's not Javascript. I don't know what you want to say.

LinusBorg avatar Feb 07 '20 08:02 LinusBorg

That's not Javascript. I don't know what you want to say.

Sorry my reply is sort. It mean that:

  • Work fine with $router injection
$router.push(....)
  • Don't work with $route injection (I tried new injection with this.$route)
$route.params

thearabbit avatar Feb 07 '20 09:02 thearabbit

My complete code to create injection of this.$route (NOT this.$router)

// composables/use-route.js
import { provide, inject } from '@vue/composition-api'

const RouteSymbol = Symbol()

export function provideRoute(route) {
  provide(RouteSymbol, route)
}

export default function useRoute() {
  const route = inject(RouteSymbol)
  if (!route) {
    // throw error, no store provided
  }

  return route
}
------------------
// App.vue
export default () => {
  setup(props, { root: { $route} }) {
    provideRoute($route)
------------
// Usage in component
import useRoute from './user-route'

export default () => {
  setup(){
    const route = useRoute()
    console.log(route) 
  ......
}
--------------- Result -----------
name: null
meta: {}
path: "/"
hash: ""
query: {}
params: {}
fullPath: "/"
matched: []

Don't work, my route

  {
    path: '/login',
    name: 'login',
    component: () => import('../../ui/pages/Login.vue'),
    meta: {
      layout: 'Public',
    },
  },

thearabbit avatar Feb 07 '20 09:02 thearabbit

@LinusBorg are your examples from https://github.com/vuejs/rfcs/issues/70#issuecomment-508199992 still valid? as i understand we clearly dont want to execute this plugin setup function with every component:

app.mixin({ 
  setup() {
    provide(key, swal)
  }
})

is there a way to only hook into the setup of the App component?

backbone87 avatar Feb 29 '20 13:02 backbone87

@backbone87 If you mean implicitly, I don't know how. For Vue 2.x plugins, I usually check if this === this.$root or something similar inside global mixins.

Now that plugins apply at app-level, not globally, it would be nice if we could hook into app "lifecycle hooks", e.g. app.onMounted, app.onUnmounted, inside plugins.

leopiccionia avatar Feb 29 '20 15:02 leopiccionia

A simple alternative to the current api.

// hooks/use-router.ts
import { computed, getCurrentInstance } from '@vue/composition-api';

export const useRouter = () => {
	const vm = getCurrentInstance();

	if (!vm) {
		throw new ReferenceError('Not found vue instance.');
	}

	const route = computed(() => vm.$route);

	return { route, router: vm.$router } as const;
};

negezor avatar Mar 25 '20 16:03 negezor

@negezor you don't even need the computed. Just go for a getter:

return { 
  get route() { return vm.$route }, 
  router: vm.$router 
}

Bonus chatter: there are differences between these 2 approaches:

  1. computed caches its value (useful if the computation is costly);
  2. computed is reactive itself and will be watched instead of its source (useful if there are many consumers watching the same computed);
  3. the result of an accessor will be proxified automatically, which won't be the case for the computed (can be a pitfall).

jods4 avatar Mar 25 '20 16:03 jods4

@jods4 I chose computed for just one reason:

  • We can use useRouter() in components that always remain mounted. And who needs to know the route changes.

negezor avatar Mar 25 '20 17:03 negezor

@negezor what's the difference with the getter? You can use it in components that remain mounted and they will know when it changes just the same.

jods4 avatar Mar 25 '20 17:03 jods4

@jods4 In two cases:

  • Destruction at the beginning of setup()
  • Use in the template without the $ prefix
setup() {
  const { route } = useRouter();

  return { route }
}

negezor avatar Mar 25 '20 17:03 negezor

Extracting the value is not reactive. I meant, what's the difference with this getter:

return { 
  get route() { return vm.$route }, 
  router: vm.$router 
}

jods4 avatar Mar 25 '20 17:03 jods4

@jods4 the property itself is not reactive, but after watchEffect should the value be subtracted again? UDP: I made a test sandbox for an example

negezor avatar Mar 25 '20 17:03 negezor