I want to use useRoute inside my component which is called in onMounted hook.
Something like
import { checkUserAuth } from '#/common/CheckUserAuth'
onMounted(async () => {
await checkUserAuth()
})
And CheckUserAuth.ts is:
import { useRouter } from 'vue-router'
const router = useRouter() // here router is undefined
export const checkUserAuth = async () => {
const userStore = useUserStore()
const userApi = new UserApi()
const token = localStorage.getItem(TOKEN_NAME)
const router = useRouter() // and here too router is undefined
if (!token) {
await router.push({ name: LOGIN_PAGE_ROUTE })
return
}
const userData = await userApi.fetchMasterInfo()
userStore.setUser(userData)
await router.push({ name: DASHBOARD_ROUTE })
}
I don't understand why the router is indefined everywhere and is it possible to solve this without passing the router as an argument? (i want to make the checkUserAuth function fully encapsulated)
i know i can fix it like
const router = useRouter()
onMounted(async () => {
await checkUserAuth(router)
})
export const checkUserAuth = async (router: Router) => {
await router.push({ name: DASHBOARD_ROUTE })
}
But it's not good solution
The API of useRouter must be called in setup, as mentioned in the official document. You can see this point in https://router.vuejs.org/zh/api/#userouter(zh document mentioned it).
Maybe you can write code like this:
import { useRouter } from 'vue-router'
export const useCheckUserAuth = () => {
const userStore = useUserStore()
const router = useRouter()
returnv aysnc function checkUserAuth() {
const userApi = new UserApi()
const token = localStorage.getItem(TOKEN_NAME)
if (!token) {
await router.push({ name: LOGIN_PAGE_ROUTE })
return
}
const userData = await userApi.fetchMasterInfo()
userStore.setUser(userData)
await router.push({ name: DASHBOARD_ROUTE })
}
}
And call it in setup:
const checkUserAuth = useCheckUserAuth()
onMounted(() => {
checkUserAuth()
})
hope it can help you.
Composables are supposed to be used directly in setup, unless their implementation allows for other usage, this needs to be determined for each case.
Since checkUserAuth uses composables, this makes it a composable either, in case it needs to be used in mounted hook, it needs to return a function that allows this:
const useUserAuth = () => {
const router = useRouter()
const userStore = useUserStore()
const userApi = new UserApi()
return {
async check() {...}
}
}
Alternatively, checkUserAuth shouldn't use composables. useUserStore doesn't have restrictions that are inherent to composable, and useRouter can be replaced with an import of router instance.
Related
I can't access my routes from the store.
There may be a good explanation for this.
I use Vuejs3 and Pinia
My store :
import {defineStore} from 'pinia'
import {useRoute} from "vue-router";
type navigationState = {
selectedNavigationItem: INavigationItem | null,
selectedNavigationPage: INavigationPage | null,
}
export const useNavigationStore = defineStore('navigationStore', {
state: () => ({
/**
* when the user clicks on an element of the navbar we store the navigation item here
*/
selectedNavigationItem: null,
/**
* when the user clicks on an element of the sidebar we store the navigation page here
*/
selectedNavigationPage: null,
} as navigationState),
actions: {
/**
* Set Selected navigation page
* #param navigationPage
* #type INavigationPage
*/
setSelectedNavigationPage(navigationPage: INavigationPage | null) {
console.log(useRoute())
this.selectedNavigationPage = navigationPage
},
},
})
when I do a console log like in the method setSelectedNavigationPage
I have an undefined
useRoute and useRouter must be used in Vue components and specifically setup method or inside script setup.
useRouter Docs
useRoute Docs
If you want to access the router though, you can simply import it:
router-file
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(),
routes: [/* ... */]
})
then in your pinia store you can import and use the router from that file:
import { defineStore } from 'pinia'
import router from './router'
export const myStore = defineStore('myStore', () => {
// router.push
// router.replace
})
EDIT: Thanks for sophiews for pointing this out.
Just found out that we have different way to defineStore: Setup Stores
// src/stores/user.js
import { defineStore } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import api from './api.js'
export const useUserStore = defineStore('User', () => { // use function
const route = useRoute()
const router = useRouter()
const login = async () => {
await api.POST('login', {username, password})
router.replace({name: 'home'})
}
return { login } // IMPORTANT: need to return anything we need to expose
})
Old answer
You can add router as Pinia plugin
// src/main.js
import { createPinia } from 'pinia'
import { createApp, markRaw } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import Home from './views/HomePage.vue'
import Api from './api.js' // my axios wrapper
const app = createApp(App)
// I usually put this in a separate file src/router.js and export the router
const routes = [
{ path: '/', component: HomePage },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
const pinia = createPinia()
pinia.use(({ store }) => {
store.router = markRaw(router)
store.api = markRaw(Api)
})
app
.use(pinia)
.use(router)
.mount('#app')
Then router and api are available on this
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('User', {
state: () => ({}),
actions: {
async login() {
await this.api.POST('login', {username, password})
this.router.replace({name: 'home'})
}
}
})
Note that you can't call this.router with arrow function.
login: async () => {
this.router.replace({name: 'home'}) // error
}
For typescript user, to correctly get type for this.router and this.api:
// src/global.d.ts
import { Router } from 'vue-router'
import Api from './api'
export { }
declare global {
}
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router,
api: typeof Api
}
}
I found this way on pinia github.
https://github.com/vuejs/pinia/discussions/1092
But I still don't know how to add this.route to Pinia.
Future reader, please comment if you know how to do it.
You could wrap the process of instantiating a store within a factory/function, this will allow you to expand the stores capabilities regarding your custom needs. Below you can see that we can instantiate a store referencing the urql client and the router object.
Have a look:
export class StoreManager {
static _instances: any[] = [];
public static spawnInstance(
id: string,
storeType?: EStoreType,
clientHandle?: ClientHandle,
routerHandle?: Router,
) {
if (StoreManager._instances.find((i) => i.id === id)) {
const store = StoreManager._instances.find((i) => i.id === id).instance;
return store;
} else {
const store = StoreManager.initStore(
id,
storeType,
clientHandle ?? null,
routerHandle ?? null,
);
StoreManager._instances.push({
id: id,
instance: store,
storeType: storeType,
});
return store;
}
}
public static initStore(
id: string,
storeType: EStoreType,
clientHandle: ClientHandle | null,
routerHandle: Router | null,
) {
const baseState = {
_meta: {
storeType: storeType,
isLoading: true,
},
_client: clientHandle,
_router: routerHandle,
};
const baseActions = {
async query(query: any, variables: any[] = []) {
// use urql client
},
};
const baseGetters = {
storeType: (state) => state._meta.storeType,
getCurrentRoute: (state) => {
if (!state._router) {
throw new RouterNotSetException(
`This store does not have a router set up`,
);
}
return state._router.currentRoute.fullPath.replace('/', '');
},
};
switch (storeType) {
case EStoreType.DEFAULT:
return defineStore({
id: `${id}`,
state: () => ({
...baseState,
}),
actions: {
...baseActions,
},
getters: {
...baseGetters,
},
});
default:
throw new StoreTypeNotFoundException(
`Expected valid 'EStoreType', got ${storeType}`,
);
}
}
}
Within your VueComponent a store instance would be spawned like this:
const store = StoreManager.spawnInstance(
uuidv4(),
EStoreType.DEFAULT,
useClientHandle(),
useRouter(),
)();
Trying to pass route query to axios request, but it is empty..
route.query returns empty in mounted. route.queryreturns {"filter[city]": "Vilnius" } in axios then
nextTick doesn't solve issue. Any tips?
import { ref, onMounted, nextTick } from 'vue';
import axios from 'axios';
import { useRouter, useRoute } from 'vue-router';
export default {
setup() {
const router = useRouter();
const route = useRoute();
onMounted(() => {
console.log(route.query); // log is {}
fetchApartments();
});
function fetchApartments() {
console.log(route.query); // log is {}
axios.get('/api/apartments').then(response => {
console.log(route.query); // log is { "filter[city]": "Vilnius" }
});
}
}
}
Route navigation is asynchronous. You need to wait for router.isReady for queries to be available
import {useRouter, useRoute} from 'vue-router';
export default {
setup() {
const router = useRouter();
const route = useRoute();
onMounted(async () => {
await router.isReady();
console.log(route.query);
});
}
}
Update your code like this:
...
import { computed } from 'vue'
...
and inside setup()
const route = useRoute();
const query = computed(() => route.query)
The missing part here is computed property.
I want to use fresh Vuex store in every test, so I'm looking to replace default store with test store. Reason is that my Router is using store getter to check if user is allowed to access specific route.
The problem is that it always uses default store, which is not used by test component. I've tried to mock #\store, but looks like I'm missing something.
#\store\index.ts
import {createStore, Store} from 'vuex'
import {State} from '#vue/runtime-core'
import auth from "#/store/modules/auth";
export const createNewStore = (): Store<State> => (createStore({
modules: {
auth,
},
}))
export default createNewStore()
#\router\index.ts
import {createMemoryHistory, createRouter, createWebHistory, Router, RouteRecordRaw} from 'vue-router'
import store from '#/store';
const routes: Array<RouteRecordRaw> = [ ... ]
export const createNewRouter = (): Router => {
const isServer = Boolean(typeof module !== 'undefined' && module.exports)
const router = createRouter({
history: isServer ? createMemoryHistory() : createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => !record.meta.doesntRequiresAuth) && !store.getters['auth/isAuthenticated']) {
next({name: 'login'})
} else {
next()
}
})
return router
}
export default createNewRouter()
Current implementation looks like this:
#\views\__tests__\Login.spec.ts
import {render, screen} from '#testing-library/vue';
import AppComponent from '../../App.vue';
import userEvent from "#testing-library/user-event";
import {createNewRouter} from "#/router";
const {createNewStore} = jest.requireActual('#/store');
import {Router} from "vue-router";
describe('Login.vue', () => {
let router : Router
const renderComponentWithDependencies = async () => {
const mockStore = createNewStore();
jest.doMock('#/store', () => ({
__esModule: true,
default: mockStore,
}));
router = createNewRouter();
await router.push('/')
render(AppComponent, {
global: {
plugins: [router, mockStore],
}
})
}
beforeEach(async () => {
await renderComponentWithDependencies()
fetchMock.resetMocks();
});
it('User logs with correct username and pin', async () => {
const username = 'Pavel'
const pin = 'test'
fetchMock.mockResponseOnce(JSON.stringify({}));
fetchMock.mockResponseOnce(JSON.stringify({token: "123"}));
await screen.findByLabelText("Имя пользователя")
userEvent.type(screen.getByLabelText("Имя пользователя"), username)
await userEvent.click(screen.getByText('Запросить пин'))
userEvent.type(await screen.findByLabelText('Пин код'), pin)
await userEvent.click(screen.getByText('Войти'))
await router.isReady()
await screen.findByText('You are on home page')
})
})
Turns out there is way better solution I wasn't aware of, which doesn't require mocking - use jest.resetModules():
import {render, screen} from '#testing-library/vue';
import AppComponent from '../../App.vue';
import userEvent from "#testing-library/user-event";
import router from "#/router";
import store from '#/store';
describe('Login.vue', () => {
const renderComponentWithDependencies = async () => {
await router.push('/')
// We use App component instead of Login to test routing to homepage at the end of the login
render(AppComponent, {
global: {
plugins: [router, store],
}
})
}
beforeEach(async () => {
await renderComponentWithDependencies()
fetchMock.resetMocks();
jest.resetModules();
});
it('User logs with correct username and pin', async () => {
const username = 'Pavel'
const pin = 'test'
fetchMock.mockResponseOnce(JSON.stringify({}));
fetchMock.mockResponseOnce(JSON.stringify({token: "123"}));
await screen.findByLabelText("Имя пользователя")
userEvent.type(screen.getByLabelText("Имя пользователя"), username)
await userEvent.click(screen.getByText('Запросить пин'))
userEvent.type(await screen.findByLabelText('Пин код'), pin)
await userEvent.click(screen.getByText('Войти'))
await router.isReady()
await screen.findByText('You are on home page')
})
})
I defined a route in vue:
/users/:userId
Which point to UserComponent:
<template>
<div>{{username}}</div>
</template>
and I use computed from #vue/composition-api to get the data.
the problem is when the route change to another userId, by navigate to another user, the user in the html template not changed as what I expected. also it doesn't do redirect when the the user is not in the list.
So what I can do to fix that?
here is my code:
<template>
<div>{{username}}</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, getCurrentInstance } from '#vue/composition-api';
export const useUsername = ({ user }) => {
return { username: user.name };
};
export default defineComponent({
setup(props, { root }) {
const vm = getCurrentInstance();
const userToLoad = computed(() => root.$route.params.userId);
const listOfUsers = [
{ userId: 1, name: 'user1' },
{ userId: 2, name: 'user2' },
];
const user = listOfUsers.find((u) => u.userId === +userToLoad.value);
if (!user) {
return root.$router.push('/404');
}
const { username } = useUsername({ user });
return { username };
},
});
</script>
You can just do this:
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
// Now you can access params like:
console.log(route.params.id);
}
};
From the vue-router documentation:
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
if (!user) {
router.push({
name: '404',
query: {
...route.query
}
})
}
}
}
}
You can pass the parameters as props to your components. Props are reactive by default.
This is how the route configuration could look like:
{
path: '/users/:userId',
name: Users,
component: YourComponent
},
You can then use the props in your component with watchEffect()
<template>
<div>{{username}}</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, getCurrentInstance, watchEffect } from '#vue/composition-api';
export const useUsername = ({ user }) => {
return { username: user.name };
};
export default defineComponent({
props: {userId: {type: String, required: true },
setup(props, { root }) {
const vm = getCurrentInstance();
const user = ref()
const userToLoad = computed(() => props.userId);
const listOfUsers = [
{ userId: 1, name: 'user1' },
{ userId: 2, name: 'user2' },
];
watchEffect(() => user.value = listOfUsers.find((u) => u.userId === +userToLoad.value))
if (!user) {
return root.$router.push('/404');
}
const { username } = useUsername({ user });
return { username };
},
});
</script>
watchEffect() will run immediately when defined and when reactive dependencies.change
A had the same problem. I use vue 2 and #vue/composition-api
My resolution:
Created: src/router/migrateRouterVue3.js
import { reactive } from '#vue/composition-api';
import router from './index';
const currentRoute = reactive({
...router.currentRoute,
});
router.beforeEach((to, from, next) => {
Object.keys(to).forEach(key => {
currentRoute[key] = to[key];
});
next();
});
// eslint-disable-next-line import/prefer-default-export
export function useRoute() {
return currentRoute;
}
after that, I can usage:
// import { useRoute } from 'vue-router';
import { useRoute } from '#/router/migrateRouterVue3';
Resolution for you:
// replace:
// const userToLoad = computed(() => root.$route.params.userId);
// to:
import { useRoute } from '#/router/migrateRouterVue3';
//...
const route = useRoute();
const userToLoad = computed(() => route.params.userId);
function useRoute() {
const vm = getCurrentInstance()
if (!vm) throw new Error('must be called in setup')
return vm.proxy.$route
}
https://github.com/vuejs/composition-api/issues/630
The following useRoute hook will make route reactive so that it's doable:
const route = useRoute();
const fooId = computed(()=>route.params.fooId);
let currentRoute = null;
export const useRoute = () => {
const self = getCurrentInstance();
const router = self.proxy.$router;
if (!currentRoute) {
const route = { ...self.proxy.$route };
const routeRef = shallowRef(route);
const computedRoute = {};
for (const key of Object.keys(routeRef.value)) {
computedRoute[key] = computed(() => routeRef.value[key]);
}
router.afterEach((to) => {
routeRef.value = to;
});
currentRoute = reactive(computedRoute);
}
return currentRoute;
};
The vue2-helpers package provides a useRoute function you can use in Vue 2.7 (and 2.6, 2.5 also).
Installation
# Vue 2.7
$ npm install vue2-helpers#2
# Vue 2.5 and 2.6
$ npm install vue2-helpers#1
Usage
import { useRoute } from 'vue2-helpers/vue-router';
const route = useRoute();
const id: string | undefined = route.params.id;
const { proxy } = getCurrentInstance();
then use proxy to access $router or $route
Add please this code: watchEffect(() => userToLoad);
I am trying to pre-fetch some data and update Vuex before client-side kicks in.
store/index.js
export const state = () => ({});
export const getters = {};
export const actions = {
async nuxtServerInit ({ dispatch }) {
await dispatch('nasa/getImages');
}
};
store/moduleName.js
import fetch from 'node-fetch';
export const state = () => ({
images: []
});
export const mutations = {
storeImages(state, data) {
state.images = [];
state.images.push(...data);
console.log(state.images[0]); <- this logs in the terminal
}
}
export const actions = {
getImages(store) {
return fetch('api/url').then(response => {
response.json().then(function(data) {
store.commit('storeImages', data.collection.items.slice(0, 24));
});
});
}
}
My mutation gets triggered by nuxtServerInit and I am getting the first element logged in the terminal on page load. My store in the client-side however, is empty.
What am I missing?
With help from a friend we have managed to fix this issue by removing node-fetch and adding axios to Vuex instead.
The only change made was in store/moduleName.js which now looks like:
import Axios from 'axios'
export const state = () => ({
images: []
});
export const mutations = {
storeImages(state, data) {
state.images.push(...data);
}
}
export const actions = {
async getImages(store) {
let res = await Axios.get('api/url');
store.commit('storeImages', res.data.collection.items.slice(0, 24));
}
}