Currently I'm using vue-query by tanstack in my vue 3 app with the following code structure.
I have the following code in my todo/edit.vue
<template>
<v-container>
<v-row cols="12">
<v-text-field label="Name" v-model="formData.title"></v-text-field>
</v-row>
<v-row cols="12">
<v-text-field label="Price" v-model="formData.price"></v-text-field>
</v-row>
<v-row cols="12">
<v-col md="6" offset="6">
<v-btn color="success" #click="submit">Submit</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { useQuery, useMutation, useQueryClient } from "#tanstack/vue-query"
import { fetchProduct, update} from "../services/product"
import { reactive } from "vue"
import { useRoute, useRouter } from "vue-router"
import type { IProduct } from "#/interface"
const route = useRoute()
let formData = reactive<IProduct>({
id: "",
title: "",
price: "",
category: "",
email: "",
})
const {
isLoading,
isError,
data: loadedData,
error,
refetch,
} = useQuery({
queryKey: ["getOneProduct", route.params.id],
queryFn: fetchProduct,
select: (data: any) => (formData = data),
})
const mutation = useMutation({
mutationFn: ({id, data}) => {
return update(id, data)
},
onSuccess: () => {
alert("success")
},
})
const submit = () => {
mutation.mutate({id:formData.id, data:formData})
}
</script>
My services/product.ts contains
import { API } from "../util/API"
import type { IProduct } from "../interface/IProduct"
export interface IProductsResponse {
products?: Array<IProduct>
total?: number
skip?: number
limit?: number
}
export const fetchProducts = async (): Promise<IProductsResponse> => {
return await API.get<IProductsResponse, any>(`/products?limit=10&select=id,title,price,category`)
}
export const fetchProduct = async (param: any): Promise<IProduct> => {
const id = param.queryKey[1]
const response = await API.get<IProduct, any>(`products/${id}`)
return response
// return await API.get<IProduct, any>(`/products/`)
}
export const update = async (id: any, data: any): Promise<IProduct> => {
return API.put(`/products/${id}`, data)
}
With the above code setup I'm getting the following errors:
Property 'id' does not exist on type 'void'.
Property 'data' does not exist on type 'void'.
Refereeing to the line:
mutationFn: ({id, data}) => {
return update(id, data)
},
And error message
Argument of type '{ id: string | number; data: { id: string | number; title: string; price: string | number; category: string; email: string; }; }' is not assignable to parameter of type 'void'.
Refereeing to line:
mutation.mutate({id:formData.id, data:formData})
What would be the correct way to perform PUT/PATCH operation with "#tanstack/vue-query" ?
Related
to add data to the server by axios POST method, I receive 500 HTTP error.
it occurs when the request is made from vuex store.
when the request is form component there isn't any problem.
export
default {
name: 'addCategoury',
data: () => ({
name: '',
src: '',
description: '',
}),
methods: {
async AddCat() {
const newCategory = {
categoryName: this.name,
description: this.description,
imageUrl: this.src,
}
let result = await this.$store.dispatch('AddCategory', newCategory)
if (result.data) {
alert('shode');
} else {
alert('result failed')
}
}
}
}
////////////////////in store js////////////////////
import Vue from 'vue'
import Vuex from 'vuex'
import api from '../services/API'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
categories: []
},
getters: {
},
mutations: {
get_category(state, cat) {
}
},
actions: {
async AddCategory(newCategory) {
try {
let result = await api().post('/category/create',newCategory);
console.log(result)
if (result.data) {
alert('ok')
return result
}
} catch (error){
return error
}
}
},
})
////////////////////in API js////////////////////
import axios from 'axios'
export default () => {
return axios.create({
baseURL: 'https://limitless-lake-55070.herokuapp.com'
})
}
<template>
<v-container class="align-text-center">
<v-form class="form">
<v-container>
<v-row>
<v-col cols="7">
<v-text-field outlined v-model="name" label="name" required>
</v-text-field>
</v-col>
<v-col cols="7">
<v-text-field outlined v-model="src" label="image Source" required>
</v-text-field>
</v-col>
<v-col cols="7">
<v-text-field outlined v-model="description" label="description"></v-text-field>
</v-col>
<v-col cols="12">
<v-btn #click="AddCat">
ADD
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</v-container>
</template>
to add data to the server by axios POST method, I receive 500 HTTP error.
it occurs when the request is made from vuex store.
when the request is form component there isn't any problem.
// in the component
methods:{
async AddCat() {
const newCategory = {
categoryName: this.name,
description: this.description,
imageUrl: this.src,
}
let result = await this.$store.dispatch('AddCategory', newCategory)
if(result.data){
alert('dode');
} else {
alert('result failed')
}} }
// in the store.js
import api from '../services/API'
actions: {
async AddCategory(newCategory) {
try {
let result = await api().post('/category/create',newCategory);
console.log(result)
if (result.data) {
alert('ok')
return result
}
} catch (error){
return error
}
}
},
// API
import axios from 'axios'
export default () => {
return axios.create({
baseURL: 'https://limitless-lake-55070.herokuapp.com'
})
}
So I see you're using $store.dispatch, I am assuming you want to use vue actions. I will explain my code after the solution below:
Your component file looks fine to me, please make sure to add the import file too. It plays a big role in your files too, we won't be able to debug without that.
api.js // would be your file where you're setting the default URL for Axios
import axios from "axios";
export default () => {
return axios.create({
baseURL: "https://limitless-lake-55070.herokuapp.com"
});
};
store.js // Actions object is missing from your question
import api from "../plugins/api";
export const actions = {
async AddCategory(newCat) {
try {
let res = await api().post('/category/create',newCat)
let result = res.status;
if (result == 200 || result == 201) {
alert('ok')
}
} catch(error) {
console.log('dd')
}
},
}
I have this component that accepts an array as a property:
import {
defineComponent,
getCurrentInstance,
toRefs,
watch,
} from "#vue/composition-api";
import { RecommendationAnswer, RecommendationQuestion } from "#models";
import { useCalculateInitialCount } from "./calculate-count";
import { useGetAnsweredQuestions } from "./list-questions";
export default defineComponent({
name: "StepThree",
emits: ["onSelect"],
props: {
products: {
type: Array,
required: false,
default: () => [],
},
questions: {
type: Array,
required: false,
default: () => [],
},
},
setup(props) {
const instance = getCurrentInstance();
const { products, questions } = toRefs(props);
watch(
products,
(currentProducts: any[]) => {
if (!currentProducts) return;
const currentQuestions = <RecommendationQuestion[]>questions.value;
useCalculateInitialCount(currentProducts, currentQuestions);
},
{
immediate: true,
}
);
const selectAnswer = (answer: RecommendationAnswer) => {
answer.selected = !answer.selected;
questions.value.forEach((question: RecommendationQuestion) => {
question.selected = !!question.answers.find(
(item: RecommendationAnswer) => item.selected
);
});
const answeredQuestions = useGetAnsweredQuestions(
<RecommendationQuestion[]>questions.value
);
instance.proxy.$emit("onSelect", {
step: 3,
questions: answeredQuestions,
});
};
return { selectAnswer };
},
});
The watch is triggered whenever the products array changes (which happens outside of this component).
I can see that the watch fires and then the function useCalculateInitialCount fires, which updates the count property on an answer.
This is displayed in the template:
<v-col cols="6">
<base-fade-up class="row" :duration="0.1" tag="div">
<v-col
class="text-center"
cols="12"
v-for="question in questions.slice(
0,
Math.ceil(questions.length / 2)
)"
:key="question.id"
>
{{ question.title }}
<v-card
class="w-100"
outlined
#click="selectAnswer(answer)"
v-for="answer in question.answers"
:key="answer.id"
>
<v-card-text class="text-center">
{{ answer.title }} ({{ answer.count }})
</v-card-text>
</v-card>
</v-col>
</base-fade-up>
</v-col>
When the component loads, the watch fires and the counts are displayed correctly:
But when the products update, even though I see the changes in the console.log:
The template does not update.
Does anyone know how I can get around this?
I think it's because your array does not have a new item, so for the watcher is the same array with the same amount of items even if one of them has changed. I'm not sure why you have to watch a property but if you need to watch all the changes in the array you can try to make a copy of the array first and then watch that copied array
I figured a work around for this, by created a computed property instead of watching the products.
The entire code looks like this:
import {
computed,
defineComponent,
getCurrentInstance,
toRefs,
watch,
} from "#vue/composition-api";
import { RecommendationAnswer, RecommendationQuestion } from "#models";
import { useCalculateInitialCount } from "./calculate-count";
import { useGetAnsweredQuestions } from "./list-questions";
export default defineComponent({
name: "StepThree",
emits: ["onSelect"],
props: {
products: {
type: Array,
required: false,
default: () => [],
},
questions: {
type: Array,
required: false,
default: () => [],
},
},
setup(props) {
const instance = getCurrentInstance();
const { products, questions } = toRefs(props);
const questionsWithCount = computed(() => {
const currentProducts = <any[]>products.value;
const currentQuestions = [...(<RecommendationQuestion[]>questions.value)];
if (!currentProducts?.length || !currentQuestions?.length) return;
useCalculateInitialCount(currentProducts, currentQuestions);
return currentQuestions;
});
const selectAnswer = (answer: RecommendationAnswer) => {
answer.selected = !answer.selected;
questions.value.forEach((question: RecommendationQuestion) => {
question.selected = !!question.answers.find(
(item: RecommendationAnswer) => item.selected
);
});
const answeredQuestions = useGetAnsweredQuestions(
<RecommendationQuestion[]>questions.value
);
instance.proxy.$emit("onSelect", {
step: 3,
questions: answeredQuestions,
});
};
return { questionsWithCount, selectAnswer };
},
});
This fixed the issue, because in the template I use the questionsWithCount instead of the questions
Im trying to use vuex to make things easier, overall it's fine, but Im stuck when using a getter with param from an other getter.
main code :
<template>
<v-container>
<v-card v-for="(order,i) in getOrders" :key="i" class="cart-cards text-left">
<v-card-title>
{{getMealById(order.meal_id).name}}
</v-card-title>
<v-btn v-on:click="addQuantity(order)">
+
</v-btn>
<h1>
{{order.quantity}}
</h1>
<v-btn #click="reduceQuantity(order)">
-
</v-btn>
</v-card>
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
data: () => ({
}),
created() {
this.fetchOrders();
},
mounted() {
},
methods: {
...mapActions(["fetchOrders"]),
addQuantity(order) {
order.quantity += 1;
this.updateOrders(order);
},
reduceQuantity(order) {
if (order.quantity > 0) {
order.quantity -= 1;
this.updateOrders(order);
}
},
},
computed: {
...mapGetters(["getOrders", "getMealById"]),
},
};
order.js :
import axios from 'axios'
import url from '../../config.js'
const state = {
all_orders: [],
}
const getters = {
getOrders : (state)=>state.all_orders,
}
const actions = {
async fetchOrders({commit}) {
const response = await axios.get("http://" + url + "/orders")
commit('setOrders',response.data)
},
async updateOrders({commit},payload) {
const response = await axios.put("http://" + url + "/orders/"+payload.id,payload)
commit('setOrders',response.data)
},
}
const mutations = {
setOrders: (state,orders)=>{
state.all_orders = orders
},
}
export default {
state,
getters,
actions,
mutations
}
meal.js
import axios from 'axios'
import url from '../../config.js'
const state = {
all_meals: [],
}
const getters = {
getMeals: (state) => state.all_meals,
getMealById: (state) => (id) => {
return state.all_meals.find(todo => todo.id === id)
}
}
const actions = {
async fetchMeals({ commit }) {
const response = await axios.get("http://" + url + "/meals")
commit('setMeals', response.data)
},
}
const mutations = {
setMeals: (state, meals) => {
state.all_meals = meals
},
}
export default {
state,
getters,
actions,
mutations
}
So when iam accessing the vue from a link, no error, but when I load the url by itself, an error occur and the getMealById dont trigger
overall Is their a good practice for "waiting" for response on state/actions call ?
Thanks in advance !!!
In component, you can check if getMeals returns a non-empty array, then render the v-for loop:
<template>
<v-container v-if="getMeals().length > 0">
<v-card v-for="(order,i) in getOrders" :key="i" class="cart-cards text-left">
<v-card-title>
{{getMealById(order.meal_id).name}}
</v-card-title>
</v-card>
</v-container>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
data: () => ({
}),
created() {
this.fetchOrders();
},
mounted() {
},
methods: {
...mapActions(["fetchOrders"]),
},
computed: {
...mapGetters(["getMeals", "getOrders", "getMealById"]),
},
};
I have a snackbar from Vuetify. It's in default.vue and the vuex store controls the v-model, message and color:
DefaultSnackBar.vue
<template>
<v-container>
<v-snackbar
v-model="snackbarProperties.show"
:color="snackbarProperties.color"
timeout="7000"
multi-line
>
{{ snackbarProperties.message }}
<template v-slot:action="{ attrs }">
<v-btn
text
v-bind="attrs"
#click="hideSnackbar"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import { mapActions } from "vuex";
import { mapGetters } from "vuex";
export default {
methods :{
...mapActions("Snackbar",["showSnackbar","hideSnackbar"]),
},
computed: {
...mapGetters("Snackbar",["snackbarProperties"])
},
}
</script>
Snackbar.js
export const state = () => ({
message: "",
color: "",
show: false,
});
export const getters = {
snackbarProperties: state => {
return state;
},
}
export const mutations = {
showSnackbar: (state, payload) => {
state.message = payload.message;
state.color = payload.color;
state.show = true;
},
hideSnackbar: (state) => {
state.message = "";
state.color = ""
state.show = false;
},
}
export const actions = {
showSnackbar({ commit }, payload) {
commit('showSnackbar', payload)
},
hideSnackbar({ commit }) {
commit('hideSnackbar')
}
}
When I call showSnackbar({...}) the bar appears correctly with no errors, but when it disappears (timeout is reached) is get this error and everything crashes
do not mutate vuex store state outside mutation handlers
I think it's because when the bar disappears the component changes the value of the v-model it's attached to but I'm not sure how to work around this.
I found the answer from this vue forum:
Use an action with the setTimeout code in it. Then in the timeout
commit the mutation. Mutations should be synchronous which is why
using a timeout in them is throwing a warning.
I've updated Snackbar.js to suit:
export const state = () => ({
message: "",
color: "",
show: false,
});
export const getters = {
snackbarProperties: state => {
return state;
},
}
export const mutations = {
showSnackbar: (state, payload) => {
state.message = payload.message;
state.color = payload.color;
state.show = true;
},
hideSnackbar: (state) => {
state.message = "";
state.color = ""
state.show = false;
},
}
export const actions = {
showSnackbar({ commit }, payload) {
commit('showSnackbar', payload)
setTimeout(() => {
commit('hideSnackbar')
}, 500);
},
hideSnackbar({ commit }) {
commit('hideSnackbar')
}
}
try this if you need showing multiple
<template>
<div class="text-center">
<v-snackbar
v-for="(snackbar, index) in snackbars.snackbars.filter(
(s) => s.isVisible
)"
:key="snackbar.text + Math.random()"
v-model="snackbar.isVisible"
:color="snackbar.color"
:timeout="-1"
:right="true"
:top="true"
:style="`top: ${index * 60}px`"
>
<v-row no-gutters>
<v-col md="11" sm="11">
{{ snackbar.text }}
</v-col>
<v-col md="1" sm="1">
<v-btn class="mx-2" icon small #click="hideNotify(index)">
<v-icon color="error"> mdi-close </v-icon>
</v-btn>
</v-col>
</v-row>
</v-snackbar>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({ snackbars: 'notification' }),
},
methods: {
hideNotify(index) {
this.$store.dispatch('notification/HIDE_NOTIFY_WITH_INDEX',index)
},
},
}
</script>
add this into the vuex as notification.js
export const state = () => ({
snackbars: [],
})
export const mutations = {
SET_SNACKBAR(state, snackbar) {
state.snackbars = state.snackbars.concat(snackbar)
},
HIDE_NOTIFY_WITH_INDEX(state,index) {
if (index in state.snackbars) {
state.snackbars.splice(index, 1)
}
},
HIDE_NOTIFY(state) {
state.snackbars = []
},
}
export const actions = {
SET_SNACKBAR({ commit }, snackbar) {
snackbar.isVisible = true
snackbar.color = snackbar.color || 'dark'
commit('SET_SNACKBAR', snackbar)
setTimeout(() => {
commit('HIDE_NOTIFY')
}, 6000)
},
HIDE_NOTIFY_WITH_INDEX({ commit },index) {
commit('HIDE_NOTIFY_WITH_INDEX',index)
},
}
Here is how CourseDescriptionPage.vue looks
import CourseCover from './CourseDescription/CourseCover.vue'
import WhyJoin from './CourseDescription/WhyJoin.vue'
import CourseStructure from './CourseDescription/CourseStructure.vue'
export default {
props: ['id'],
data () {
return {
hasDetails: false
}
},
created () {
this.$store.dispatch('loadCourseDetails', this.id).then(() => {
this.hasDetails = true
})
},
computed: {
course () {
return this.$store.state.courseDetails[this.id]
}
},
components: {
CourseCover,
WhyJoin,
CourseStructure
},
name: 'CourseDescriptionPage'
}
<template>
<div v-if="hasDetails">
<course-cover :courseTitle="course.title" :courseDuration="course.duration"></course-cover>
<why-join :courseTitle="course.title" :courseJobs="course.jobs"></why-join>
<course-structure :lectureList="course.lectureList"></course-structure>
</div>
</template>
Here is how my store looks
import Vuex from 'vuex'
import * as firebase from 'firebase'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
courseDetails: {},
loading: false
},
mutations: {
setCourseDetails (state, payload) {
const { id, data } = payload
state.courseDetails[id] = data
},
setLoading (state, payload) {
state.loading = payload
}
},
actions: {
loadCourseDetails ({commit}, payload) {
commit('setLoading', true)
firebase.database().ref(`/courseStructure/${payload}`).once('value')
.then((data) => {
commit('setCourseDetails', {
id: payload,
data: data.val()
})
commit('setLoading', false)
})
.catch(
(error) => {
console.log(error)
commit('setLoading', false)
}
)
}
}
Here is how my CourseCover.vue looks
export default {
props: {
courseTitle: {
type: String,
required: true
},
courseDuration: {
type: String,
required: true
}
},
name: 'CourseCover'
}
<template>
<v-jumbotron
src="./../../../static/img/course_cover_background.png">
<v-container fill-height>
<v-layout align-center>
<v-flex>
<h3>{{ courseTitle }}</h3>
<span>{{ courseDuration }}</span>
<v-divider class="my-3"></v-divider>
<v-btn large color="primary" class="mx-0" #click="">Enroll</v-btn>
</v-flex>
</v-layout>
</v-container>
</v-jumbotron>
</template>
I think there is something wrong with the way I am using props here but I couldn't figure out.
The data is loaded in store by the firebase that I know for sure because it shows in Vue dev tools but I just couldn't understand why Vue is complaining about that.
Thanks in advance.
course is undefined on component initialize ,so then you should return an empty object:
computed: {
course () {
return this.$store.state.courseDetails[this.id] || {}
}
},