Fetching before navigation with multiple components using vue-router - vue.js

Trying to figure out what is the best way to fetch some data before navigating to some routes that I have.
My routes are:
let routes = [
{
path: '/',
component: require('./views/Home.vue')
},
{
path: '/messages',
component: require('./views/Messages.vue'),
},
{
path: '/posts',
component: require('./views/Post.vue'),
},
{
path: '/login',
component: require('./views/Login.vue')
},
{
path: '/dashboard',
component: require('./views/Dashboard.vue'),
}
];
For /messages, /posts and /dashboard I want to fetch some data and while doing that show a spinner.
Vue-router docs suggest to use beforeRouteEnter, Fetching Before Navigation
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
}
But my question is, must I implement this fetch logic in all my three view components?
Since it's the same data being fetched I don't want to repeat the logic in every component.
The beforeRouteEnter isn't called in my root component where I have this <router-view></router-view> otherwise I feel that it would be the best place to have this logic.
I'm really new to vue and this one is a really hard nut to crack for me.

Are you aware of vuex? Since you need to share the same data on three different pages, which sounds like a job for vuex store. Basically you can import the store into your router, and set data in the store instead of on the component, and then you can conditionally fetch the data by checking the status of the data in the store and if the data already exists, don't fetch. Something like the following:
import store from './store'
beforeRouteEnter (to, from, next) {
if (store.state.post === null) {
getPost(to.params.id, (err, post) => {
store.dispatch('setPost', post)
next()
})
}
}
Assume your store has a state called post and action called setPost.
const store = new Vuex.Store({
state: { post: null },
actions: {
setPost ({ commit }, post) {
// set the post state here
}
}
})

Related

Dynamically add a route in a Nuxt3 middleware

I have a Nuxt3 project where I'd like to add new routes based on an API call to a database. For example, let's say a user navigates to /my-product-1. A route middleware will look into the database and if it finds an entry, it will return that a product page should be rendered (instead of a category page, for example).
This is what I came up with:
export default defineNuxtPlugin(() => {
const router = useRouter()
addRouteMiddleware('routing', async (to) => {
if (to.path == '/my-awesome-product') {
router.addRoute({
component: () => import('/pages/product.vue'),
name: to.path,
path: to.path
})
console.log(router.hasRoute(to.path)) // returns TRUE
}
}, { global: true })
})
To keep it simple, I excluded the API call from this example. The solution above works, but not on initial load of the route. The route is indeed added to the Vue Router (even on the first visit), however, when I go directly to that route, it shows a 404 and only if I don't reload the page on the client does it show the correct page when navigated to it for the second time.
I guess it has something to do with the router not being updated... I found the following example in a GitHub issue, however, I can't get it to work in Nuxt3 as (as far as I'm aware) it doesn't provide the next() method.
When I tried adding router.replace(to.path) below the router.addRoute line, I ended up in an infinite redirect loop.
// from https://github.com/vuejs/vue-router/issues/3660
// You need to trigger a redirect to resolve again so it includes the newly added
route:
let hasAdded = false;
router.beforeEach((to, from, next) => {
if (!hasAdded && to.path === "/route3") {
router.addRoute(
{
path: "/route3",
name: "route3",
component: () => import("#/views/Route3.vue")
}
);
hasAdded = true;
next('/route3');
return;
}
next();
});
How could I fix this issue, please?
Edit:
Based on a suggestion, I tried using navigateTo() as a replacement for the next() method from Vue Router. This, however, also doesn't work on the first navigation to the route.
let dynamicPages: { path: string, type: string }[] = []
export default defineNuxtRouteMiddleware((to, _from) => {
const router = useRouter()
router.addRoute({
path: to.path,
name: to.path,
component: () => import ('/pages/[[dynamic]]/product.vue')
})
if (!dynamicPages.some(route => route.path === to.path)) {
dynamicPages.push({
path: to.path,
type: 'product'
})
return navigateTo(to.fullPath)
}
})
I also came up with this code (which works like I wanted), however, I don't know whether it is the best solution.
export default defineNuxtPlugin(() => {
const router = useRouter()
let routes = []
router.beforeEach(async (to, _from, next) => {
const pageType = await getPageType(to.path) // api call
if (isDynamicPage(pageType)) {
router.addRoute({
path: to.path,
name: to.path,
component: () => import(`/pages/[[dynamic]]/product.vue`),
})
if (!routes.some(route => route.path === to.path)) {
routes.push({
path: to.path,
type: pageType,
})
next(to.fullPath)
return
}
}
next()
})
})
I suggest you use dynamic routing within /page directory structure - https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
The [slug] concept is designed exactly for your usecase. You don't need to know all possible routes in advance. You just provide a placeholder and Nuxt will take care of resolving during runtime.
If you insist on resolving method called before each route change, the Nuxt's replacement for next() method you're looking for is navigateTo
https://nuxt.com/docs/api/utils/navigate-to
And I advise you to use route middleware and put your logic into /middleware/routeGuard.global.ts. It will be auto-executed upon every route resolving event. The file will contain:
export default defineNuxtRouteMiddleware((to, from) => {
// your route-resolving logic you wanna perform
if ( /* navigation should happen */ {
return navigateTo( /* your dynamic route */ )
}
// otherwise do nothing - code will flow and given to.path route will be resolved
})
EDIT: However, this would still need content inside /pages directory or some routes created via Vue Router. Because otherwise navigateTo will fail, as there would be no route to go.
Here is an example of one possible approach:
https://stackblitz.com/edit/github-8wz4sj
Based on pageType returned from API Nuxt route guard can dynamically re-route the original URL to a specific slug page.

Insert localstorage with vuex

My script I'm using axios and vuex but it was necessary to make a change from formData to Json in the script and with that it's returning from the POST/loginB2B 200 api, but it doesn't insert in the localstorage so it doesn't direct to the dashboard page.
**Auth.js**
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
async LogIn({commit}, user) {
await axios.post("loginB2B", user);
await commit("setUser", user.get("email"));
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
**Login.vue**
methods: {
...mapActions(["LogIn"]),
async submit() {
/*const User = new FormData();
User.append("email", this.form.username)
User.append("password", this.form.password)*/
try {
await this.LogIn({
"email": this.form.username,
"password": this.form.password
})
this.$router.push("/dashboard")
this.showError = false
} catch (error) {
this.showError = true
}
},
},
app.vue
name: "App",
created() {
const currentPath = this.$router.history.current.path;
if (window.localStorage.getItem("authenticated") === "false") {
this.$router.push("/login");
}
if (currentPath === "/") {
this.$router.push("/dashboard");
}
},
};
The api /loginB2B returns 200 but it doesn't create the storage to redirect to the dashboard.
I use this example, but I need to pass json instead of formData:
https://www.smashingmagazine.com/2020/10/authentication-in-vue-js/
There are a couple of problems here:
You do a window.localStorage.getItem call, but you never do a window.localStorage.setItem call anywhere that we can see, so that item is probably always empty. There also does not seem to be a good reason to use localStorage here, because you can just access your vuex store. I noticed in the link you provided that they use the vuex-persistedstate package. This does store stuff in localStorage by default under the vuex key, but you should not manually query that.
You are using the created lifecycle hook in App.vue, which usually is the main component that is mounted when you start the application. This also means that the code in this lifecycle hook is executed before you log in, or really do anything in the application. Instead use Route Navigation Guards from vue-router (https://router.vuejs.org/guide/advanced/navigation-guards.html).
Unrelated, but you are not checking the response from your axios post call, which means you are relying on this call always returning a status code that is not between 200 and 299, and that nothing and no-one will ever change the range of status codes that result in an error and which codes result in a response. It's not uncommon to widen the range of "successful" status codes and perform their own global code based on that. It's also not uncommon for these kind of endpoints to return a 200 OK status code with a response body that indicates that no login took place, to make it easier on the frontend to display something useful to the user. That may result in people logging in with invalid credentials.
Unrelated, but vuex mutations are always synchronous. You never should await them.
There's no easy way to solve your problem, so I would suggest making it robust from the get-go.
To properly solve your issue I would suggest using a global navigation guard in router.js, mark with the meta key which routes require authentication and which do not, and let the global navigation guard decide if it lets you load a new route or not. It looks like the article you linked goes a similar route. For completeness sake I will post it here as well for anyone visiting.
First of all, modify your router file under router/index.js to contain meta information about the routes you include. Load the store by importing it from the file where you define your store. We will then use the Global Navigation Guard beforeEach to check if the user may continue to that route.
We define the requiresAuth meta key for each route to check if we need to redirect someone if they are not logged in.
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
}
];
// Create a router with the routes we just defined
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// This navigation guard is called everytime you go to a new route,
// including the first route you try to load
router.beforeEach((to, from, next) => {
// to is the route object that we want to go to
const requiresAuthentication = to.meta.requiresAuth;
// Figure out if we are logged in
const userIsLoggedIn = store.getters['isAuthenticated']; // (maybe auth/isAuthenticated if you are using modules)
if (
(!requiresAuthentication) ||
(requiresAuthentication && userIsLoggedIn)
) {
// We meet the requirements to go to our intended destination, so we call
// the function next without any arguments to go where we intended to go
next();
// Then we return so we do not run any other code
return;
}
// Oh dear, we did try to access a route while we did not have the required
// permissions. Let's redirect the user to the login page by calling next
// with an object like you would do with `this.$router.push(..)`.
next({ name: 'Login' });
});
export default router;
Now you can remove the created hook from App.vue. Now when you manually change the url in the address bar, or use this.$router.push(..) or this.$router.replace(..) it will check this function, and redirect you to the login page if you are not allowed to access it.

Best solution for getting {id} from url?

I'm trying to create a feature where individuals can create Listings, and other users can submit an application. ATM I can't quite figure out how to get the ID from the URL of:
'http://localhost:3000/listings/2/'
I have a hunch that this needs Vue router and $route.params.id? Basically I want in my form.listing the ID of the listing in which users are applying to. In this case it would be '2'
My folder structure is .../listings/_id/index.vue
This is my backend for the user_applications model in DRF:
listing = models.ForeignKey(Listings, on_delete=models.CASCADE, default="1")
Here's the frontend: I was hoping I could use the props value in script, but it seems like it only works in template, oh well.
data: () => ({
form: {
role: null,
company: null,
status: "Pending"
// listing: listings.id
}
}),
props: ["listings"],
computed: {
...mapGetters(["loggedInUser"])
},
methods: {
submit() {
this.$axios
.post("/api/v1/apps/", this.form)
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
}
}
You are on the right track when thinking of using VueRouter. Lets say that your component name is Listing.
import VueRouter from 'vue-router';
import Listing form './path/to/ListingComponent.vue';
const router = new VueRouter({
routes: [
{
path: '/listing/:id',
component: Listing,
// return parms.id as listingId for the `Listing` component.
props: route => ({ listingId: route.params.id })
}
]
})
In this way you have listingId passed to the props of Listing.
Yes, it's as easy as this.$route.params.id. You can invoke $route.params.id directly in your template, without using this. Don't use arrow functions in your component, though! They strip out the value of this! Also, all props and data attributes are available on this as well. So this.listing and this.$route are available, not only in your template, but in your Vue component's methods and computed properties, as well.
Try this approach, instead:
data() {
return {
form: {
role: null,
company: null,
status: "Pending",
listing: this.listings.id
}
}
},

A cyclic dependency between 'router' and 'store' in Vuex app

I have a vue.js app with a router that prevents the pages from been open without authorization using the following code:
import Router from 'vue-router';
import store from '../store/index';
function guardAuth(to, from, next) {
if (store.state.authorizationToken) {
next();
} else {
next({
name: 'login',
query: { redirect: to.fullPath },
});
}
}
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'toroot',
redirect: 'login',
},
{
path: '/overview',
component: Overview,
beforeEnter: guardAuth,
},
....
and a store mutation that is called when an API call fails:
import axios from 'axios';
import Cookies from 'js-cookie';
import router from '../router/index';
export default new Vuex.Store({
state: {
mutations: {
handleApiFail(state, err) {
if (err && !axios.isCancel(err) && state.authorizationToken) {
// Block subsequent logout calls.
state.authorizationToken = null;
// Clear the token cookie just in case.
Cookies.set('authorizationToken', null);
// Stop the current and subsequent requests.
state.cancellationSource.cancel('Authorization token has expired.');
router.push({ name: 'login', query: { expired: '1', redirect: window.location.pathname } });
}
},
as you can see from the code above 'router' imports 'store' and 'store' imports 'router' and as far as I see this causes 'store' to be undefined inside 'guardAuth()'. Obviously, I can get rid of this cyclic dependency by moving 'handleApiFail' to a separate '.js' file, but I am not sure that it is a good idea. Is there a better solution or some common approach for haling this sutiation? Should 'handleApiFail' be a mutation or a simple function? Can a mutation use 'router'? Do I really need to get rid of the cyclic dependency (for example, in C++ I does not)?
It be better handleapi fail in separate function than mutation. and if you want to check it before entering route. you could use beforeEnter() on your route.
check this docs about beforeEnter or another route properties
Store mutation methods should not perform any logic at all. Stores are only used to hold your global application state, they should not perform any logic like authorizing the user or navigating through your application. What you'll want to do is move the logic out of the store and into the component that does the authorization check. From there just do something like $store.commit('unauthorized') and $store.commit('authorized', user). Should look like this:
sendAuthRequest.then(
(success) => {
$store.commit('authorized', <userVariable>);
$router.push(...);
}, (failure) => {
$store.commit('unauthorized');
$router.push(...);
}
);

Access store from inside beforeEnter

I have the following in a file filled with routes
import store from '../../../store'
{
name: 'auth.logout',
path: 'logout',
meta: { requiresAuth: false },
beforeEnter: (to, from, next) => {
store.dispatch('logout')
return '/'
}
}
However in trying to run the code store is undefined. How can I access the Vuex store to execute the following code?
how can you also call a user defined action passing in the store.dispatch method? i.e. logout({ dispatch }) { ....
in store.js, are you exporting new vuex.Store instance or just the plain object and aseemblyng it on main.js? And is the path ('../../../store') right?
Also, if it is a SPA, I don't see any point to have a '/logout' route, why not just have a logout method on your navbar component that executes the same logic (store.dispatch('logout'), router.push('/')).
Off topic: it has no sense to enter on a 'logout' endpoint with requiresAuth: false