Cannot use Vue-Router to get the parameters in the URL - vue.js

Today, when trying to use Vue-Router (in Vue-CLI) to get URL parameters, I encountered difficulties ($route.query is empty), the code is as follows.
Code purpose: Get the parameters carried after the URL (such as client_id in "http://localhost:8080/#/?client_id=00000000000077")
Project file structure:
router/index.js:
App.vue(Get part of the code for URL parameters):
The running result of this part of the code:

I'm not sure why $router.currentRoute and $route aren't matching up, but you could simply use $router.currentRoute.query.client_id if you need it in mounted().
Another workaround is to use a $watch on $route.query.client_id:
export default {
mounted() {
const unwatch = this.$watch('$route.query.client_id', clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}
Or watch in the Composition API:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
mounted() {
console.log({
route: this.$route,
router: this.$router,
})
},
setup() {
const route = useRoute()
const unwatch = watch(() => route.query.client_id, clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}

Related

Vue3 / Vuex State is empty when dispatching action inside of lifecycle hook inside of test

We're using the composition API with Vue 3.
We have a Vuex store that, amongst other things, stores the currentUser.
The currentUser can be null or an object { id: 'user-uuid' }.
We're using Vue Test Utils, and they've documented how to use the store inside of tests when using the Composition API. We're using the store without an injection key, and so they document to do it like so:
import { createStore } from 'vuex'
const store = createStore({
// ...
})
const wrapper = mount(App, {
global: {
provide: {
store: store
},
},
})
I have a component and before it is mounted I want to check if I have an access token and no user currently in the store.
If this is the case, we want to fetch the current user (which is an action).
This looks like so:
setup() {
const tokenService = new TokenService();
const store = useStore();
onBeforeMount(async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
});
}
I then have a test for this that looks like this:
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await shallowMount(App, {
global: {
provide: {
store
}
}
});
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
The test fails, but interestingly, the console log of the currentUser in state is not empty:
console.log src/App.vue:27
User: { id: 'user-uuid' }
Error: expect(received).toStrictEqual(expected) // deep equality
Expected: {"id": "user-uuid"} Received: null
Despite the test failure, this works in the browser correctly.
Interestingly, if I extract the logic to a method on the component and then call that from within the onBeforeMount hook and use the method in my test, it passes:
setup() {
const tokenService = new TokenService();
const store = useStore();
const rehydrateUserState = async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
};
onBeforeMount(async () => {
await rehydrateUserState();
});
return {
rehydrateUserState
};
}
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await cmp.vm.rehydrateUserState();
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
Any ideas on why this works when extracted to a method but not when inlined into the onBeforeMount hook?

Why is 'route.params.id' unavailable/undefined in setup method?

I'm working with vue-router and Vue 3. I have a view where I'd like to take the router url and use it to call a method to access an API. This method returns a promise I can use to populate my page. When calling my method with 'route.params.id', it says that the parameter is undefined. When I do console.log(route.params.id), it displays correctly in console. I've tried using a computed property instead, but I had the same issue.
Setup code:
import { ref } from "vue";
import MovieApiService from "../api/MovieApiService";
import { useRoute } from "vue-router";
export default {
setup() {
const movie = ref([]);
const route = useRoute();
MovieApiService.getMovie(route.params.id).then((response) => {
movie.value = response.data.results;
});
return {
movie,
};
},
method: {},
};
Method being called:
static getMovie(body: GetMovieByTmdbId) {
return axios.get(
`https://api.themoviedb.org/3/movie/${body.id}?api_key=${apiKey}`
);
}
Here's what I tried to compute the property instead, with the same result.
setup() {
const route = useRoute();
const id = computed(()=>{return route.params.id})
const movie = ref([]);
getMovie(id).then((response) => {
movie.value = response.data.results;
console.log(movie.value);
});
How should I ensure this value is available when I call my method?
You are giving route.params.id as a variable to getMovie(body).
You are then using body.id in your URL. This would equal route.params.id.id which is not defined.
Use body in your URL, or change the parameter to id so is makes more sense.
Like this:
...
const route = useRoute();
MovieApiService.getMovie(route.params.id).then((response) => {
movie.value = response.data.results;
});
...
const getMovie = (id) => {
return axios.get(
`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}`
);
}

NuxtJS - Prevent fetch if data already exists in state?

I have a portfolio site built using NuxtJS and a headless Wordpress CMS. On several pages, I'm importing a mixin that looks like this:
import { mapActions, mapState } from 'vuex';
export default {
computed: {
...mapState({
galleries: state => state.portfolio.galleries[0],
})
},
methods: {
...mapActions('portfolio', ['fetchGalleries']),
},
async fetch() {
await this.fetchGalleries();
}
}
The Vuex module looks like this:
export const state = () => ({
galleries: [],
});
export const actions = {
async fetchGalleries({ commit }) {
let res = await this.$axios.$get(`${process.env.WP_API_URL}/wp/v2/media`);
const data = res.reduce((acc, item) => {
const { slug } = item.acf.category;
(acc[slug] || (acc[slug] = [])).push(item);
return acc;
}, {});
commit('setGalleries', data);
}
};
export const mutations = {
setGalleries(state, data) {
state.galleries.push(data);
}
};
fetch is being used in the mixin to return data from the api before page load. I noticed however that each time I navigate to a new page, it's running that same fetch and continually adding duplicate data to Vuex state.
How do I prevent fetch from running and continually adding duplicate data to my state if it already exists?
I'm not sure why this was tripping me up so much, but I figured out a very simple solution.
async fetch() {
if (this.galleries.length) return;
await this.fetchGalleries();
}
Just added a conditional return statement as the first line within the fetch function.

Check if vuex-persist has restored data in Nuxt project

I have added Vuex-Persist and Vuex-ORM to my Nuxt project. When the application starts for the first time I want to add some boilerplate data.
In my default.vue layout I have added a created function to add this dummy data.
<script>
import Project from '../models/Project';
export default {
created () {
// I'm Using Vuex-Orm to check if there are any projects stored
if (Project.query().count() === 0) {
Project.insert({ data: [{ title: 'My first project' }] })
}
}
}
</script>
When the application is reloaded and opens for the second time, I would expect that Project.query().count()
returns 1. But it will always return 0 because vuex-persist isn't done restoring the local data yet.
According to the docs this would be the solution
import { store } from '#/store' // ...or wherever your `vuex` store is defined
const waitForStorageToBeReady = async (to, from, next) => {
await store.restored
next()
}
store.defined is undefined and same goes for this.$store.
That comment highlights my exact question "Where is my vuex store defined?"
i think you have to put the whole thing in a route guard.
create a route-guard.js plugin like this. but I haven't tested the whole thing, hope it helps you further.
export default function ({app}) {
const waitForStorageToBeReady = async (to, from, next) => {
await store.restored
next()
}
app.router.beforeEach(waitForStorageToBeReady);
}
Another option is to put a getter in computed and watch it:
export default {
computed: {
persistState() {
return this.store.getter['get_persis_state'];
}
},
watch: {
persistState(newVal) {
// check new val
}
}
}
I followed the instructions from vuex-persist for Nuxt and made a plugin file, like this:
// ~/plugins/vuex-persist.js
import VuexPersistence from 'vuex-persist'
export default ({ store }) => {
window.onNuxtReady(() => {
new VuexPersistence({
/* your options */
}).plugin(store);
});
}
window.onNuxtReady caused the plugin to be loaded after all other code had run. So I didn't matter if I made a router-guard.js plugin or tried it in the layout/default.vue file.
I ended up with the quick fix:
// ~/plugins/vuex-persist.js
import VuexPersistence from 'vuex-persist'
export default ({ store }) => {
window.onNuxtReady(() => {
new VuexPersistence().plugin(store);
if (Project.query().count() === 0) {
Project.insert({ data: [{ title: 'My first project' }] })
}
});
}

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')