Updated from my original question. The content of the question remains the same, i only updated the scope. I am looking forwdard to call my Auth store, which calls a composable, which calls i18n.
The problem begins when i try to call my Auth Store from the router.beforeEach callback.
I can freely use this composable, except for the usecase inside the router guard callback.
router.ts
import { createRouter, createWebHistory } from '#ionic/vue-router'
import routes from '~pages'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
router.beforeEach((to) => {
const publicPages = ['/login']
const authRequired = !publicPages.includes(to.path)
const auth = useAuthStore()
if (authRequired && !auth.auth) {
auth.returnUrl = to.fullPath
return '/login'
}
})
export default router
main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { IonicVue } from '#ionic/vue'
import { defineCustomElements } from '#ionic/pwa-elements/loader'
import { createI18n } from 'vue-i18n'
import messages from '#intlify/unplugin-vue-i18n/messages'
import App from './App.vue'
import router from '~/modules/router'
// Omited several CSS imports
import 'uno.css'
defineCustomElements(window)
const pinia = createPinia()
const i18n = createI18n({
legacy: false,
locale: 'en',
messages,
})
const app = createApp(App)
.use(IonicVue)
.use(i18n)
.use(pinia)
.use(router)
router.isReady().then(() => { app.mount('#app') })
Yet i am seeing this error: Uncaught SyntaxError: Must be called at the top of a setup function
My Pinia store is a as simple as an authentication store would look like:
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { Auth } from '~/types'
export const useAuthStore = defineStore('auth', () => {
const { success, error, warn } = useToastManager()
const auth = ref<Auth | null>(JSON.parse(localStorage.getItem('auth')!) ?? null)
const returnUrl = ref('')
const { httpClient } = useHttpClient()
const isLogin = computed(() => {
return auth.value !== null
})
function login(username: string, password: string): Promise<boolean> {
isLoading.value = true
return new Promise((resolve, reject) => {
httpClient.post<Auth>('/api/Account/Login',
{
username,
password,
}).then(async (response) => {
auth.value = response.data
localStorage.setItem('auth', JSON.stringify(response.data))
success('Login', 'Silent')
resolve(true)
}).catch(async (e) => {
error('Login', e, e.code)
})
})
}
async function logoff() {
auth.value = null
}
return { login, logoff, isLogin, auth, returnUrl }
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))
Here is my composable for managing toasts.
import { toastController } from '#ionic/vue'
import { alertCircleOutline, checkmarkCircleOutline, helpCircleOutline } from 'ionicons/icons'
import { useI18n } from 'vue-i18n'
const errorTimeout = 3000
const successTimeout = 1000
export function useToastManager() {
const { t } = useI18n()
const success = async (action: string, message: string) => {
isLoading.value = false
const toast = await toastController.create({
message: t('success'),
duration: successTimeout,
position: 'bottom',
color: 'success',
icon: checkmarkCircleOutline,
})
if (message === 'Silent')
return
if (action === 'Fetch')
return
await toast.present()
}
const error = async (action: string, message: string, code?: string) => {
isLoading.value = false
let showMessage = ''
switch (code) {
case 'ERR_NETWORK':
showMessage = t('network_error')
break
case 'ERR_BAD_RESPONSE':
showMessage = t('invalid_operation')
break
case 'ERR_BAD_REQUEST':
showMessage = t('invalid_operation')
if (action === 'Login')
showMessage = t('err')
break
case 'ECONNABORTED':
showMessage = t('timeout_failed_operation')
break
default:
showMessage = 'No err code found'
}
const toast = await toastController.create({
message: showMessage,
duration: errorTimeout,
position: 'top',
color: 'danger',
icon: alertCircleOutline,
// onclick: detailedAlert(action, message)
})
await toast.present()
}
const warn = async (action: string, message: string) => {
isLoading.value = false
const toast = await toastController.create({
message: `${message}`,
duration: errorTimeout,
position: 'bottom',
color: 'warning',
icon: helpCircleOutline,
})
await toast.present()
}
return { success, error, warn }
}
It is probably caused by calling const { t } = useI18n() in the composable. i18n is complaining that you call the use-function not from a setup function: 'Uncaught SyntaxError: Must be called at the top of a setup function'.
In a large project where there we use options and composition api, we use the global t function as a work-around.
Make a i18n module i18n.ts:
import { createI18n } from 'vue-i18n'
import messages from '#intlify/unplugin-vue-i18n/messages'
export const i18n = createI18n({
legacy: false,
locale: 'en',
messages,
})
Import this in your main.ts and call it like you in your example (.use(i18n)).
Then in your composable, import the i18n module and get the global t function from it:
import { i18n } from '..../i18n'; // import your i18n.ts file
const { t } = i18n.global;
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(),
)();
I'm trying to get Pinia to work in Nuxt with SSR (server-side rendering).
When creating a page without Pinia, it works:
<script>
import { reactive, useFetch, useContext } from '#nuxtjs/composition-api'
export default {
setup() {
const { $axios } = useContext()
const invitesStore = reactive({
invites: [],
loading: true,
})
useFetch(async () => {
invitesStore.loading = true
await $axios.$get('invite/registermember').then((result) => {
invitesStore.loading = false
invitesStore.invites = result.invites
})
})
return {
invitesStore,
}
},
}
</script>
But when introducing Pinia, I get the error "Converting circular structure to JSON --> starting at object with constructor 'VueRouter'"
I'm using Pinia this way:
// /store/invitesStore.js
import { defineStore } from 'pinia'
// useStore could be anything like useUser, useCart
export const useInvitesStore = defineStore({
// unique id of the store across your application
id: 'storeId',
state() {
return {
invites: [],
loading: true,
}
},
})
<script>
import { useInvitesStore } from '#/store/invitesStore'
import { reactive, onMounted, useFetch, useContext } from '#nuxtjs/composition-api'
export default {
setup() {
const { $axios } = useContext()
const invitesStore = useInvitesStore()
useFetch(async () => {
invitesStore.loading = true
await $axios.$get('invite/registermember').then((result) => {
invitesStore.loading = false
invitesStore.invites = result.invites
})
})
return {
invitesStore,
}
},
}
</script>
Is it possible to get this to work? How?
I created an App with some components and using the redux-saga in the following component:
// AlphaScreen.js
import ... // react & react-native
import { useSelector, useDispatch } from 'react-redux';
import { getUser } from '../redux/ducks/user';
const AlphScreen = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUser());
}, [dispatch]);
const users = useSelector((state) => state.user.user);
console.log(users);
return (
<View><Text>Blah</Text></View>
);
}
// redux/ducks/user.js
export const SET_USER = "SET_USER";
export const GET_USER = "GET_USER";
export const setUser = (user) => ({
type: SET_USER,
user // user: user
});
export const getUser = () => ({
tye: GET_USER
});
const initialState = {
user: undefined
};
const userReducer = (state = initialState, action) => {
switch(action.type) {
case SET_USER:
const {user} = action;
return {...state, user:user};
default:
return state;
}
};
export default userReducer;
// redux/defaultStore.js
import {applyMiddleware, combineReducers, createStore} from 'redux'
import createSagaMiddleware from "redux-saga";
import counterReducer from './ducks/counter'
import userReducer from './ducks/user';
import { watcherSaga } from './sagas/saga';
const reducer = combineReducers({
user: userReducer
});
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(reducer, applyMiddleware(...middlewares));
sagaMiddleware.run(watcherSaga)
export default store;
// redux/sagas/saga.js
import { takeLatest } from 'redux-saga/effects';
import { handleGetUsers } from './handlers/user';
import { GET_USER } from '../ducks/user';
export function* watcherSaga() {
yield takeLatest(GET_USER, handleGetUsers); //<- getting error takeLatest$1 requires a saga parameter
}
// redux/sagas/handlers/user.js
import { call, put } from "redux-saga/effects";
import { setUser } from "../../ducks/user";
import { requestGetUser } from "../requests/user";
export function* handleGetUser(action) {
try {
const response = yield call(requestGetUser);
const { data } = response;
yield put(setUser(data));
} catch (error) {
console.log(error);
}
}
// redux/sagas/request/user.js
import axios from 'axios'
const requestGetUser = () => {
return axios.request({
method: "get",
url:"https://jsonplaceholder.typicode.com/users"
});
}
export { requestGetUser };
But I have following error:
takeLatest$1 requires a saga parameter
at node_modules/#redux-saga/core/dist/io-1d6eccda.js:37:10 in check
at node_modules/#redux-saga/core/dist/redux-saga-effects.dev.cjs.js:386:2 in validateTakeEffect
at node_modules/#redux-saga/core/dist/redux-saga-effects.dev.cjs.js:402:22 in takeLatest$1
at src/redux/sagas/saga.js:6:10 in watcherSaga
at node_modules/#redux-saga/core/dist/redux-saga-core.dev.cjs.js:1161:17 in next
at node_modules/#redux-saga/core/dist/redux-saga-core.dev.cjs.js:1112:6 in proc
at node_modules/#redux-saga/core/dist/redux-saga-core.dev.cjs.js:1371:19 in immediately$argument_0
at node_modules/#redux-saga/core/dist/redux-saga-core.dev.cjs.js:60:15 in immediately
at [native code]:null in runSaga
at src/redux/configureStore.js:16:0 in <global>
at App.js:7:0 in <global>
at node_modules/expo/AppEntry.js:3:0 in <global>
at http://192.168.1.154:19000/node_modules/expo/AppEntry.bundle?platform=android&dev=true&hot=false&minify=false:141908:3 in global code
The above error occurred in task watcherSaga
Any suggestions for fixing the error? Thanks
To me it looks like a typo, handleGetUsers vs handleGetUser :)
So I dispatch my redux-saga action once from my react-native app and it makes two API calls. I'm trying to figure out why this is, and how to only have it send one.
App.js
const initFetch = async () => {
const userToken = await AsyncStorage.getItem("userToken");
dispatch(fetchLiked({ page: 0, search: "", userToken }));
};
useEffect(() => {
initFetch();
}, []);
configureStore.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import AsyncStorage from "#react-native-community/async-storage";
import likedReducer from "./reducers/liked";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/rootSaga";
const rootReducer = combineReducers({
liked: likedReducer,
});
const persistConfig = {
key: "primary",
storage: AsyncStorage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const sagaMiddleware = createSagaMiddleware();
export default () => {
let store = createStore(persistedReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
let persistor = persistStore(store);
return { store, persistor };
};
rootSaga.js
import { take, call, all } from "redux-saga/effects";
import { watchFetchLikedSaga } from "./likedSaga";
export default function* rootSaga() {
yield all([watchFetchLikedSaga()]);
}
likedSaga.js
import { takeLatest, call, put } from "redux-saga/effects";
import Server from "../../utils/Server";
import { fetchLikedSuccess } from "./../actions/liked";
import { types } from "../actions/types";
function* asyncFetchLiked(data) {
console.log("sending async fetch");
const { page, search, userToken } = data.payload;
try {
const response = yield call(() =>
Server.get("/api/titles/getliked", {
headers: { "auth-token": userToken },
params: { page: page, search: search },
})
);
yield put(fetchLikedSuccess(response.data));
} catch (e) {
console.log(e);
}
}
export function* watchFetchLikedSaga() {
yield takeLatest(types.SEND_REQUEST, asyncFetchLiked);
}
export const fetchLiked = (data) => {
return {
type: types.SEND_REQUEST,
payload: data,
};
};
actions/liked.js
export const fetchLiked = (data) => {
console.log("fetchLiked");
return {
type: types.SEND_REQUEST,
payload: data,
};
};
export const fetchLikedSuccess = (data) => {
console.log("fetchLikedSuccess");
return {
type: types.SEND_REQUEST_SUCCESS,
payload: data,
};
};
export const fetchLikedFailure = (error) => {
return {
type: types.SEND_REQUEST_FAILURE,
payload: {},
error: error,
};
};
My console.log output looks like this. You can see the action is only being dispatched once, but it is sending two async requests and calling the reducer success action twice.
fetchLiked
sending async fetch
sending async fetch
fetchLikedSuccess
fetchLikedSuccess
I have a router, Home, Login components and unit tests for the Login component.
The logic is: when user is unauthenticated, send him to Login page, once he's authenticated, send him to home page.
The logic works fine in the browser, however, when I run unit tests, I get an exception: thrown: undefined once the login component tries to navigate using this.$router.push('/');
In the console I see the message:
trying to route /login /
and then the exception is thrown once i run next();
Am I missing some setup to have the router working properly in the test environment?
Alternatively: is there a way to mock the next() function passed to the navigation guard?
Here's the router:
import VueRouter from 'vue-router';
import Home from '#/views/Home.vue';
import Login from '#/views/Login.vue';
import { state } from '#/store';
export const routes = [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/login',
name: 'login',
component: Login,
meta: {
noAuthRequired: true,
},
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
router.beforeEach((to: any, from: any, next: any) => {
console.log('trying to route', from.fullPath, to.fullPath);
const isAuthed = !!state.user.token;
if (!to.meta.noAuth && !isAuthed) {
next({ name: 'login' });
} else {
next();
}
});
export default router;
The component (relevant part):
import Vue from 'vue';
import Component from 'vue-class-component';
import { axios } from '../plugins/axios';
#Component
export default class App extends Vue {
private credentials = {
email: '',
password: '',
};
private error = '';
private async login() {
try {
const data = await axios.post('http://localhost:5000/api/v1/user/auth', this.credentials);
const token = data.data.payload;
this.$store.dispatch('setUser', { token });
this.error = '';
this.$router.push('/');
} catch (error) {
console.warn(error);
this.error = error;
}
}
}
And the unit test:
import Vue from 'vue';
import Vuetify from 'vuetify';
import AxiosMockAdapter from 'axios-mock-adapter';
import { Wrapper, shallowMount, createLocalVue } from '#vue/test-utils';
import flushPromises from 'flush-promises';
import Vuex, { Store } from 'vuex';
import { axios } from '#/plugins/axios';
import VTest from '#/plugins/directive-test';
import LoginPage from '#/views/Login.vue';
import { mainStore, state, IState } from '#/store';
import VueRouter from 'vue-router';
import router from '#/router';
describe('Login page tests', () => {
let page: Wrapper<Vue>;
let localStore: Store<IState>;
const localVue = createLocalVue();
const maxios = new AxiosMockAdapter(axios);
const vuetify = new Vuetify();
const errorMessage = 'Input payload validation failed';
const emailError = 'Invalid Email format';
const validData = {
email: 'valid#email.com',
password: 'test pass',
};
// in order for "click" action to submit the form,
// the v-btn component must be stubbed out with an HTML button
const VBtn = {
template: '<button type="submit"/>',
};
localVue.use(Vuetify);
localVue.directive('test', VTest);
localVue.use(Vuex);
localVue.use(VueRouter);
beforeAll(() => {
maxios.onPost().reply((body: any) => {
const jsonData = JSON.parse(body.data);
if (jsonData.email !== validData.email) {
return [400, {
message: errorMessage,
errors: { email: emailError },
}];
}
return [200, { payload: 'valid-token' }];
});
});
beforeEach(() => {
try {
localStore = new Vuex.Store({
...mainStore,
state: JSON.parse(JSON.stringify(state)),
});
page = shallowMount(LoginPage, {
store: localStore,
router,
localVue,
vuetify,
stubs: {
VBtn,
},
attachToDocument: true,
sync: false,
});
} catch (error) {
console.warn(error);
}
});
afterEach(() => {
maxios.resetHistory();
page.destroy();
});
const submitLoginForm = async (data: any) => {
page.find('[test-id="LoginEmailField"]').vm.$emit('input', data.email);
page.find('[test-id="LoginPasswordField"]').vm.$emit('input', data.password);
page.find('[test-id="LoginBtn"]').trigger('click');
await flushPromises();
};
it('Redirects user to home page after successful auth', async () => {
await submitLoginForm(validData);
expect(page.vm.$route.path).toEqual('/');
});
});