Vue 3 / Pinia: how to handle error, loading and data properly - vue.js

Using Fetch, and not axios, I wanna receive data in my components like that:
const userStore = useAuthStore();
const { user, error, loading } = storeToRefs(userStore);
userStore.getMe();
But I don't know how to do that.
I want that to have directly the error, data and loading state in one line because I think it's better.
But I don't wanna declare a loading like this in the store:
export const useAuthStore = defineStore({
id: "auth",
state: () => ({
user: {} as User,
loading: false,
}),
Because if I call another method related to this store (User), it will be the same Loading state. So this loading state (even the error state) will be in conflict.
If I would use Javascript and no Typescript, I would definetly replace this.user like this when fetching or when error (in the store):
async getMe() {
this.user = { loading: true };
try {
return await AuthService.getMe();
} catch (error) {
if (error) {
his.user = { error };
}
}
},
Because it's TypeScript, I can't replace the "user" state like that as I have set an Interface.
All I want, is to return a Data, Erros, Loading related to an unique action (not related to a state).
auth store:
import { defineStore } from "pinia";
import AuthService from "#/api/modules/auth";
interface User {
email: string;
first_name: string;
last_name: string;
force_password_change: boolean;
groups: string[];
has_2fa_enabled: boolean;
is_staff: boolean;
lang: string;
last_password_change: string;
permissions: string[];
session_expiry_date: string;
uid: string;
}
export const useAuthStore = defineStore({
id: "auth",
state: () => ({
user: {} as User,
loading: false,
}),
actions: {
async getMe() {
// this.user = { loading: true };
try {
return await AuthService.getMe();
} catch (error) {
if (error) {
// this.user = { error };
}
}
},
},
});
service:
import { Api } from "../apiSettings";
class AuthService {
async getMe(): Promise<any> {
return await Api.get("api/auth/me/");
}
}
export default new AuthService();
App.vue:
<script setup lang="ts">
import { useAuthStore } from "#/stores";
import { storeToRefs } from "pinia";
const userStore = useAuthStore();
const { user } = storeToRefs(userStore);
userStore.getMe();
console.log(user.value);
</script>

You can define a separate object for loading and error states and return it from the store's action along with the actual data. You can then use object destructuring to extract the loading, error, and data states in your component. Here's an example:
Auth store:
import { defineStore } from "pinia";
import AuthService from "#/api/modules/auth";
interface RequestState {
loading: boolean;
error: Error | null;
}
export const useAuthStore = defineStore({
id: "auth",
state: () => ({
user: {} as User,
}),
actions: {
async getMe(): Promise<{ data: User; requestState: RequestState }> {
const requestState: RequestState = {
loading: true,
error: null,
};
try {
const data = await AuthService.getMe();
requestState.loading = false;
return { data, requestState };
} catch (error) {
requestState.loading = false;
requestState.error = error;
return { data: {} as User, requestState };
}
},
},
});
Component:
<script setup lang="ts">
import { useAuthStore } from "#/stores";
import { storeToRefs } from "pinia";
const userStore = useAuthStore();
const { user, requestState } = storeToRefs(userStore);
// Call the action to get user data
userStore.getMe();
console.log(user.value); // {} - initial value of user
console.log(requestState.value); // { loading: true, error: null }
// Use object destructuring to get the actual user data, loading and error states
const { data, loading, error } = requestState.value;
if (loading) {
// Render a loading state
} else if (error) {
// Render an error state
} else {
// Render the user data
console.log(data);
}
</script>

Related

Catch(error) on dispatched method in store not working in Vue 3

I am working on login of a vue 3 app, both the login and registration work fine, but i still need to throw send back a meaningful response to user if login in credentials are rejected by the back-end, i have tried every possible means to log the rejection response from server to console but to no avail, the login is fine when credential is correct, but the console just stay mute when incorrect credential is entered
this is my login.vue
import store from "../store"
import { useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter()
const user = { email: '', password: '', remember : false }
let errorMsg = ref('');
async function login(ev) {
ev.preventDefault();
await store.dispatch('login', user)
.then(()=> {
router.push({
name: 'Dashboard'
})
})
.catch((err) => {
errorMsg = err.response.data.error
console.log(err)
})
}
and this is my vuex store
import {createStore} from 'vuex'
import axiosClient from "../axios";
const store = createStore({
state: {
user: {
data: {},
token: sessionStorage.getItem('TOKEN')
}
},
getters: {},
setters: {},
actions: {
register({commit}, user) {
return axiosClient.post('/register', user)
.then(({data}) => {
commit('setUser', data);
return data;
})
},
login({commit}, user) {
return axiosClient.post('/login', user)
.then(({data}) => {
commit('setUser', data);
return data;
})
},
},
mutations: {
logout: state => {
state.user.data = {};
state.user.token = null;
},
setUser: (state, userData)=> {
state.user.token = userData.token;
state.user.data = userData.user;
sessionStorage.setItem('TOKEN', userData.token)
}
},
modules: {}
})
export default store;
And here is my axios js file
import axios from "axios";
import store from "./store";
const axiosClient = axios.create({
baseURL: 'http://localhost:8000/api'
})
axiosClient.interceptors.request.use(config=> {
config.headers.Authorization = `Bearer ${store.state.user.token}`
return config;
})
export default axiosClient;
Response from backend as seen from Network Tab
{"error":"The provided credentials are incorrect","0":422}
After checking through my controller in my Laravel project, I discovered that I did not set the status code for the response properly.
Incorrect code:
if (!Auth::attempt($credentials, $remember)) {
return response([
'error'=> 'The provided credentials are incorrect',
422
]);
}
Corrected code:
if (!Auth::attempt($credentials, $remember)) {
return response([
'error'=> 'The provided credentials are incorrect',
], 422);
}
Axios does not treat the response received as a rejection; which needs to get its catch triggered.
Therefore my console.log that I had in my try/catch does not run at all.
I'm very happy we got this solved, big thanks to every one.

How to keep user logged in between page refreshes in FastAPI and Vue

I am new to vue.js, I have a simple web application(Vue frontend connected to a FastAPI backend) that a user can create an account and login, All of this works so far but when I refresh the page the user is logged out.
And console show an error:
Uncaught (in promise) TypeError: Cannot read property '$store' of undefined
What am I doing wrong? How to keep user logged in even after page refresh. Can anyone please help me?? thanks
store/index.js
import Vuex from 'vuex';
import Vue from 'vue';
import createPersistedState from "vuex-persistedstate";
import auth from './modules/auth';
// Load Vuex
Vue.use(Vuex);
// Create store
const store = new Vuex.Store({
modules: {
auth
},
plugins: [createPersistedState()]
});
export default store
store/modules/auth.js
import { postUserLogInAPI } from "../../service/apis.js";
const state = {
token: "",
expiration: Date.now(),
username: ""
};
const getters = {
getToken: (state) => state.token,
getUsername: (state) => state.username,
getFullname: (state) => state.fullname,
isAuthenticated: (state) => state.token.length > 0 && state.expiration > Date.now()
};
const actions = {
async LogIn({ commit }, model) {
await postUserLogInAPI(model).then(function (response) {
if (response.status == 200) {
commit("LogIn", response.data)
}
})
},
async LogOut({ commit }) {
commit('LogOut')
}
};
const mutations = {
LogIn(state, data) {
state.username = data.username
state.fullname = data.fullname
state.token = data.token
state.expiration = new Date(data.expiration)
},
LogOut(state) {
state.username = ""
state.fullname = ""
state.token = ""
state.expiration = Date.now();
},
};
export default {
state,
getters,
actions,
mutations
};
service/http.js
import axios from 'axios'
import { Loading, Message } from 'element-ui'
import router from '../router/index.js'
import store from '../store';
let loading
function startLoading() {
loading = Loading.service({
lock: true,
text: 'Loading....',
background: 'rgba(0, 0, 0, 0.7)'
})
}
function endLoading() {
loading.close()
}
axios.defaults.withCredentials = true
axios.defaults.baseURL = 'http://0.0.0.0:80/';
axios.interceptors.request.use(
(confing) => {
startLoading()
if (store.getters.isAuthenticated) {
confing.headers.Authorization = "Bearer " + store.getters.getToken
}
return confing
},
(error) => {
return Promise.reject(error)
}
)
axios.interceptors.response.use(
(response) => {
endLoading()
return response
},
(error) => {
Message.error(error.response.data)
endLoading()
const { status } = error.response
if (status === 401) {
Message.error('Please Login')
this.$store.dispatch('LogOut')
router.push('/login')
}
return Promise.reject(error)
}
)
export default axios
components/NavBar.vue
<script>
export default {
name: "NavBar",
computed: {
isLoggedIn: function () {
return this.$store.getters.isAuthenticated;
},
username: function () {
return this.$store.getters.getUsername;
},
fullname: function () {
return this.$store.getters.getFullname;
},
},
methods: {
async logout() {
await this.$store.dispatch("LogOut");
this.$router.push("/login");
},
},
};
</script>

TypeError: Cannot read property 'push' of undefined using Vue router and Vue 3

Guys I have a problem in Vue 3 and Vite, I'm trying to use the router but I have a problem because they didn't find it.
props: ["usuario", "senha"],
setup(props) {
const mensagemErro = ref("");
async function login() {
try {
const { data } = await services.post("auth/login", {
userName: props.usuario,
password: props.senha,
});
console.log(data);
const { token, userName } = data;
window.localStorage.setItem("token", token);
this.$router.push("/home");
} catch (error) {
console.log(error);
}
}
return {
login,
mensagemErro,
};
},
You should use the composable function useRouter to get the instance router :
import {useRouter} from 'vue-router'
export default{
props: ["usuario", "senha"],
setup(props) {
const mensagemErro = ref("");
const router=userRouter();
async function login() {
try {
const { data } = await services.post("auth/login", {
userName: props.usuario,
password: props.senha,
});
console.log(data);
const { token, userName } = data;
window.localStorage.setItem("token", token);
router.push("/home");
} catch (error) {
console.log(error);
}
}
return {
login,
mensagemErro,
};
},

How to integrate paypal Payment Button Vuejs3 Composition API (setup function)

I'm trying to integrate PayPal buttons with my Vuejs3 project using Composition API (setup ) but all what i get is errors i try to integrate it without using setup and its working fine i leave the working script down
the esseu is i couldent pass data from data to methodes
<script>
import { inject, onMounted, ref } from "vue";
export default {
data() {
return {
loaded: false,
paidFor: false,
product: {
price: 15.22,
description: "leg lamp from that one movie",
img: "./assets/lamp.jpg",
},
};
},
setup() {
const store = inject("store");
console.log(store.state.prodects_in_cart);
return { store };
},methods:{
setLoaded: function() {
this.loaded = true;
paypal_sdk
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: this.product.description,
amount: {
currency_code: "USD",
value: this.product.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
this.data;
this.paidFor = true;
console.log(order);
},
onError: err => {
console.log(err);
}
})
.render(this.$refs.paypal);
}
},
mounted: function() {
const script = document.createElement("script");
script.setAttribute('data-namespace',"paypal_sdk");
script.src ="https://www.paypal.com/sdk/js?client-id=Here i pute my Client Id";
script.addEventListener("load", this.setLoaded);
document.body.appendChild(script);
},
};
</script>
the error i get when i use setup() is
The error image
my script using setup()
setup() {
const store = inject("store");
const paypal = ref(null);
let loaded = ref(false);
let paidFor = ref(false);
const product = {
price: 15.22,
description: "leg lamp from that one movie",
img: "./assets/lamp.jpg",
};
onMounted: {
const script = document.createElement("script");
script.setAttribute("data-namespace", "paypal_sdk");
script.src =
"https://www.paypal.com/sdk/js?client-id=AXDJPmFjXpXm9HMXK4uZcW3l9XrCL36AxEeWBa4rhV2-xFcVYJrGKvNowY-xf2PitTSkStVNjabZaihe";
script.addEventListener("load", ()=>{
loaded = true;
console.log('hello adil');
paypal_sdk
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: 'this is product description',
amount: {
currency_code: "USD",
value: 120.00,
},
},
],
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
this.data;
this.paidFor = true;
console.log(order);
},
onError: (err) => {
console.log(err);
},
})
.render(paypal);
});
document.body.appendChild(script);
}
return { store ,paypal};
}
paypal is a ref. You're currently passing to paypal_sdk the ref itself and not the inner value, which would be the template ref's element. To fix this, pass the ref's .value.
Your onMounted code is not properly invoked, as it must be passed a callback.
import { onMounted, ref } from 'vue'
export default {
setup() {
const paypal = ref(null)
onMounted(/* 2 */ () => {
const script = document.createElement('script')
//...
script.addEventListener('load', () => {
paypal_sdk
.Buttons(/*...*/)
.render(paypal.value) /* 1 */
})
})
return {
paypal
}
}
}
The reason why you are getting that error is because you are using option Api onMounted life cycle hook, instead of doing that use the vue 3 life cycle hooks for onMounted.
First you will have to import it from vue like this.
<script>
import {onMounted} from 'vue'
then you are going to use it like this.
return it as a call back function
onMounted(() => {
//all your code should placed inside here and it will work
})
</script>
Here is my answer using the paypal-js npm package
<template>
<div ref="paypalBtn"></div>
</template>
<script>
import { onMounted, ref } from 'vue';
import { loadScript } from '#paypal/paypal-js';
const paypalBtn = ref(null);
onMounted(async () => {
let paypal;
try {
paypal = await loadScript({
'client-id': 'you_client_id_goes_here',
});
} catch (error) {
console.error('failed to load the PayPal JS SDK script', error);
}
if (paypal) {
try {
await paypal.Buttons().render(paypalBtn.value);
} catch (error) {
console.error('failed to render the PayPal Buttons', error);
}
}
});
</script>

Why can't I pass my user_name value into my component? (Auth)

I am trying to pass the name of the user after authentication into a Vue component, but I get a name: undefined value after load.
Here is my AuthService.js:
//config details taken from OAUTH JS doc: https://github.com/andreassolberg/jso
import { JSO, Fetcher } from 'jso';
const client = new JSO({
providerID: '<my-provider>',
default_lifetime: 1800,
client_id: '<my-client-id>',
redirect_uri: 'http://localhost:8080/',
authorization:'<my-auth-server>/oauth/authorize'
//scopes: { request: ['https://www.googleapis.com/auth/userinfo.profile'] }
});
export default {
getProfile() {
// JSO plugin provides a simple wrapper around the fetch API to handle headers
let f = new Fetcher(client);
let url = 'https://www.googleapis.com/auth/userinfo.profile';
f.fetch(url, {})
.then(data => {
return data.json();
})
.then(data => {
return data.user_name;
})
.catch(err => {
console.error('Error from fetcher', err);
});
}
};
Then, in my single file component named MainNav, I have:
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
created() {
this.name = AuthService.getProfile();
}
};
</script>
Anyone have any tips on how I can get the user_name value from the AuthService to my component? I will then need to then display the name in my nav template. Doing a console.log test works fine, just can't return it to my SFC. Also, the JSO library is here: https://github.com/andreassolberg/jso#fetching-data-from-a-oauth-protected-endpoint
Because getProfile returns nothing (undefined). I see you use es6 then you can use async functions
//config details taken from OAUTH JS doc: https://github.com/andreassolberg/jso
import { JSO, Fetcher } from 'jso';
const client = new JSO({
providerID: '<my-provider>',
default_lifetime: 1800,
client_id: '<my-client-id>',
redirect_uri: 'http://localhost:8080/',
authorization:'<my-auth-server>/oauth/authorize'
//scopes: { request: ['https://www.googleapis.com/auth/userinfo.profile'] }
});
export default {
getProfile() {
// JSO plugin provides a simple wrapper around the fetch API to handle headers
let f = new Fetcher(client);
let url = 'https://www.googleapis.com/auth/userinfo.profile';
return f.fetch(url, {}) // return promise here
.then(data => {
return data.json();
})
.then(data => {
return data.user_name;
})
.catch(err => {
console.error('Error from fetcher', err);
});
}
};
And
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
async created() {
try {
this.name = await AuthService.getProfile();
} catch(error) {
// handle
}
}
};
</script>
Or without async (add one more then)
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
created() {
AuthService.getProfile().then((userName) => this.name = userName))
.catch((error) => { /* handle */ })
}
};
</script>