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

Provide access to the router instance and component options in "beforeRouteEnter" guard #3166

Open
bponomarenko opened this issue Apr 6, 2020 · 14 comments

Comments

@bponomarenko
Copy link

bponomarenko commented Apr 6, 2020

What problem does this feature solve?

There are some use cases, when it is required to have access to the router instance and component options inside beforeRouteEnter guard. While beforeRouteUpdate and beforeRouteLeave could get such access through this.$router and this.$options, it seems not way to achieve it in beforeRouteEnter. Importing router instance (similar to store import suggestion here) is not an option for us because we have shared set of router components, which are re-used in different vue applications, so there is no single place to import vuex store from.

Use case

I'm trying to create generic plugin for our applications which will define hook to pre-load data. In some component data pre-load will happen via vuex store, so it should be accessible in this hook.

Here is simplified code for the plugin:

export default function install(Vue) {
  Vue.mixin({
    async beforeRouterEnter(to, from, next) {
      if (componentOptions.preloadData) {
        try {
          const nextCallbackParam = await componentOptions.preloadData.call(null, router.app.$store);
          next(nextCallbackParam);
        } catch (error) {
          // Custom error for a generic error handler
          next(new DataPreloadError(error));
        }
      } else {
        next();
      }
    },
  });
};

This plugin will make it possible to define custom preloadData() { ... } component option for a generic data pre-load as part of routing process. The only missing references to make it work are componentOptions and router.

Note that it is possible to provide router access in this particular example by passing it as plugin argument – export default function install(Vue, router) { ... }. However, it might be still beneficial to have access to router in the beforeRouteEnter guard, as it will be a solution for #3157.

What does the proposed API look like?

Proposal is to add additional guardContext argument to the guard function:

beforeRouteEnter(to, from, next, context) {
 // where context is { router: ..., componentOptions: ... }
}

However if it should be implemented in different way – would be great to hear it.

@bponomarenko
Copy link
Author

bponomarenko commented Apr 15, 2020

Alternatively, access to only componentOptions should be enough if componentOptions will have access to the parent component (like vm.$options). In this case it would be possible to get access to:

  • component options themselves
  • router instance via componentOptions.parent.$router
  • vuex store via componentOptions.parent.$store

@bponomarenko
Copy link
Author

@posva I understand that you are actively working on the next version of the router, and this kind of feature can significantly improve usage of the vue-router in more complex scenarios like ours.

From what I see in the vue-router-next code, this feature can be added to the navigationGuard.ts:

export function guardToPromiseFn(
  guard: NavigationGuard,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded,
  instance?: ComponentPublicInstance | undefined,
  resolvedComponent?: RouteComponent | undefined,
): () => Promise<void> {
  ...
  // wrapping with Promise.resolve allows it to work with both async and sync guards
  Promise.resolve(guard.call(instance, to, from, next, resolvedComponent)).catch(err =>
    reject(err)
  )
}


export function extractComponentsGuards(
  matched: RouteRecordNormalized[],
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
  ...
  if (typeof rawComponent === 'function') {
    ...
    return (
      guard && guardToPromiseFn(guard, to, from, record.instances[name], resolvedComponent)()
    )
  } else {
    const guard = rawComponent[guardType]
        guard &&
          // @ts-ignore: the guards matched the instance type
          guards.push(guardToPromiseFn(guard, to, from, record.instances[name], rawComponent))
  }
}

What do you think? If that works, it looks like it can easily be backported to vue-router v3

@posva
Copy link
Member

posva commented Aug 3, 2020

This is pretty much a duplicate of #2118

Importing the router instance (or the store) do not work on SSR anyway, so it's not a solution (I've stated the opposite in older issues because I wasn't considering SSR in some issues).

Note accessing the app through the router instance is a hack. When dealing with SSR, a app with a new router and new store is created for each request (eg https://github.com/vuejs/vue-hackernews-2.0/blob/master/src/entry-client.js) allowing referencing each router and store inside of that context in beforeEach guards. In Nuxt, there is a Context concept that allows accessing a request context on server to not share information between requests.

It's important for me to understand what is this solving. Is it accessing to the store associated to the running application? What about a router being used by multiple applications, they could each having different stores and there are situations where the navigation could come from outside of an application. I could make the router instance app-aware while being accessed through this.$router (and userRouter on v4) but if someone imports the router (valid in SPA contexts) or rely on a global variable (micro frontends maybe), there is no way to detect that and it would be very confusing to debug.

Most of the time, it is possible to replace beforeRouterEnter with global router.beforeResolve, in that scenario you have access to the router and store instances. If it's not possible to consistently provide these in beforeRouteEnter, it's fair to consider a design limitation as it can be solved in a different way.

I don't think it makes sense to provide the router instance as this in beforeEach because of #2118 (comment). beforeRouteEnter is a different story, since it's inside a component and it could need access to the store as well as other global properties

@bponomarenko
Copy link
Author

Hi @posva. Thanks for coming back on this one.

This is pretty much a duplicate of #2118. Importing the router instance (or the store) do not work on SSR anyway, so it's not a solution...

I do agree with you on this one. That's why I created different ticket with alternative API proposal, which suppose to solve the same issue. Also because this proposal is limited to beforeRouteEnter guard.

Note accessing the app through the router instance is a hack.

That is complete surprise for me, as app reference is part of the official API docs for the router. Also I wasn't aware about use cases when single router instance would be used for multiple apps, so originally proposed solution doesn't take that into account.

It's important for me to understand what is this solving.

My initial intention was to implement "data fetching before navigation" concept but with the use of Vuex store. To achieve that I need to have access to the store instance inside beforeRouteEnter hook. Since access to this is not available (because component instance is not created yet at this moment of navigation lifecycle), and I also cannot import store reference (because components are re-used between different apps with different root store instance), I was looking for an alternative ways to obtain this reference.

Most of the time, it is possible to replace beforeRouterEnter with global router.beforeResolve...

That is an interesting idea and I would investigate it, however I can already see one challenge with it. beforeRouterEnter and beforeRouteUpdate are defined in the component, encapsulating all logic for data load next to the template and the rest of component implementation. Also they clearly separate situations when component is rendered for the first time and when it is re-used between routes. Delegating data load to the router.beforeResolve breaks this encapsulation and makes it tricky (if not impossible) to distinguish newly rendered and re-used components.

Looking forward for your thoughts on this.

@posva
Copy link
Member

posva commented Aug 5, 2020

That is complete surprise for me, as app reference is part of the official API docs for the router.

That's true. Then it's not a hack... It doesn't work on multiple apps but at the same time you don't do SSR when having multiple apps, so I guess it's fine. It won't be the same on v4 (where it currently doesn't exist) because it could only point to the app returned by createApp. But that's a different topic anyway as it concerns Vue 3.

My guess is that data fetching might not belong in the routing but even with global beforeResolve, you can achieve code splitting because you can directly access the component option through the matched array in the to parameter.

@bponomarenko
Copy link
Author

bponomarenko commented Aug 5, 2020

My guess is that data fetching might not belong in the routing but even with global beforeResolve, you can achieve code splitting because you can directly access the component option through the matched array in the to parameter.

That is true, and that is something I want to investigate in our app. However, as I mentioned, there is no way to tell which of those matched components are newly-rendered and which are re-used from the previous route. Unless maybe by comparing matched arrays from to and from routes.

@posva
Copy link
Member

posva commented Aug 5, 2020

Unless maybe by comparing matched arrays from to and from routes.

Yes. They are also ordered from parent to child. You can checkout the source code to see how we do it

@bponomarenko
Copy link
Author

After some experiments, it looks like beforeRouterEnter cannot be easily replaced with router.beforeResolve.
Yes, router.beforeResolve now has access to the router and store instances, but it is missing one crucial part – possibility to pass a callback to the next function, which will be executed with the reference to the created component instance. As a result this solution is not really usable when there is need to have access to both component and store instances. I would expect that custom implementation of the functionality similar to next(vm => { ... }) would be cumbersome and error prone, since neither router.beforeResolve nor router.afterEach has access to the component instance.

@lyle45
Copy link

lyle45 commented Jun 13, 2021

+1, is really needed for components shared with lerna (can't import store instance). Got to same exact issue on our end, what's going on with this?

@bisubus
Copy link

bisubus commented Jun 27, 2021

In my case there are several apps with their router instances, and the component is unaware which one in use, so it cannot import router or app directly. And using global beforeEach or beforeResolve instead of component-level hook breaks modularity. I wonder what was the problem with providing this context to beforeRouteEnter, this wouldn't be a breaking change. From what I see, there's no way to augment this functionality from the outside without replicating the whole component router hook system.

@rijenkii
Copy link

rijenkii commented Nov 21, 2023

I have just exported the root component and import it as I need into my components:

// main.ts
export default createApp(App).use(router).mount("#app");
<script lang="ts">
import { useRouter } from "vue-router";
import main from "../main";

async function load() {
  const router = useRouter();
  await new Promise((r) => setTimeout(r, 1000));
  await router.replace({ name: "index" });
}
</script>

<script setup lang="ts">
defineOptions({
  async beforeRouteEnter() {
    await main.$root.$.appContext.app.runWithContext(load);
  },
});
</script>

Extremely not pretty, but seems working.

EDIT: useRouter is used as an example, my use case is accessing a global api provided by another plugin.

@posva
Copy link
Member

posva commented Nov 21, 2023

I think that you might find this Data Loaders RFC interesting: vuejs/rfcs#460 (comment)
It takes away the boilerplate you seem to have and no need to rely on the root component which can be easily abused

@rijenkii
Copy link

rijenkii commented Nov 21, 2023

I do find it very interesting, in fact I am trying to implement something inspired by that RFC.

Maybe I should have a looksie at how it is implemented in unplugin-vue-router.
Are global injects accessible in unplugin-vue-routers defineLoader?

@posva
Copy link
Member

posva commented Nov 21, 2023

I think they were. At least, I do plan on making them available. Be aware that the loaders are subject to change.

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

No branches or pull requests

5 participants