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

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

Open
beeplin opened this issue Jul 3, 2019 · 32 comments

Comments

@beeplin
Copy link

beeplin commented Jul 3, 2019

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

@aztalbot
Copy link

aztalbot commented Jul 3, 2019

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

@posva
Copy link
Member

posva commented Jul 3, 2019

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)

@beeplin
Copy link
Author

beeplin commented Jul 3, 2019

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.

@LinusBorg
Copy link
Member

LinusBorg commented Jul 3, 2019

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
Copy link
Member

LinusBorg commented Jul 3, 2019

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

import router from './router'

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

@phiter
Copy link

phiter commented Jul 3, 2019

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?

@LinusBorg
Copy link
Member

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.

@backbone87
Copy link

backbone87 commented Jul 3, 2019

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

@phiter
Copy link

phiter commented Jul 3, 2019

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

@LinusBorg
Copy link
Member

LinusBorg commented Jul 3, 2019

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

@thenikso
Copy link

thenikso commented Aug 2, 2019

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.

@LinusBorg
Copy link
Member

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,
  };
}

@thenikso
Copy link

thenikso commented Aug 2, 2019

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)

@LinusBorg
Copy link
Member

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

probably

@plmercereau
Copy link

plmercereau commented Jan 10, 2020

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

@thearabbit
Copy link

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?

@LinusBorg
Copy link
Member

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

@thearabbit
Copy link

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

@LinusBorg
Copy link
Member

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

@thearabbit
Copy link

thearabbit commented Feb 7, 2020

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
Copy link

thearabbit commented Feb 7, 2020

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',
    },
  },

@backbone87
Copy link

backbone87 commented Feb 29, 2020

@LinusBorg are your examples from #70 (comment) 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?

@leopiccionia
Copy link

leopiccionia commented Feb 29, 2020

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

@negezor
Copy link

negezor commented Mar 25, 2020

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

@jods4
Copy link

jods4 commented Mar 25, 2020

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

@negezor
Copy link

negezor commented Mar 25, 2020

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

@jods4
Copy link

jods4 commented Mar 25, 2020

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

@negezor
Copy link

negezor commented Mar 25, 2020

@jods4 In two cases:

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

  return { route }
}

@jods4
Copy link

jods4 commented Mar 25, 2020

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

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

@negezor
Copy link

negezor commented Mar 25, 2020

@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

@kendallroth
Copy link

kendallroth commented Aug 15, 2020

Has there been any update on this over the last few months? Several of the options in this thread no longer appear to work and I am wondering why there has been no update. I haven't even been able to get access to setup's context argument (just an empty object). The project was created using the Vue CLI and the 3 template.

UPDATE: My apologies, I was directed to the vue-router-next playground, where I found the useRouter hook. Is there any known documentation for this that I missed, or is it currently semi-hidden?

@negezor This sandbox example no longer works as of Aug 15.

@mhDuke
Copy link

mhDuke commented Aug 3, 2022

I used to use ctx.root.$route in setup function prior to vue-2.7. now with vue-2.7 ctx.root is undefined. I wonder how do you people got to solve the issue of accessing the route and it's params in a reactive way?!

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

No branches or pull requests