Vue component is not reactive - javascript

My issue is that when i make a request to delete an item from my component, the component does not automatically update to show new state.
template
<div v-for="house in allHouses" :key="house.id" >
<div class="edit-delete-wrap" v-if="house.madeByMe">
<img class="edit-delete-btn" src="/images/edit.png" alt="">
<img #click="deleteHouse(house.id)" class="edit-delete-
btn" src="/images/delete.png" alt="" srcset="">
</div>
{{house.street}}
</div>
this is an example of the template, it is card with a house details on it, there is a button to delete this item from the list.
Scripts for house card component
<script>
import {mapActions, mapGetters} from 'vuex'
export default {
name: "HouseCard",
props: ["searchHouses", "sortHouses"],
computed: {
...mapGetters(['allHouses']),
},
methods: {
...mapActions(['fetchHouses', 'houseDetail', 'deleteHouse']),
},
created(){
this.fetchHouses()
},
}
</script>
The list data comes from the allHouses houses computed function.
vuex store
import api from "../../api/houses";
const state = {
houses: [],
selectedHouse: [],
};
const getters = {
allHouses: (state) => state.houses,
selectedHouse: (state) => state.selectedHouse,
};
const actions = {
async fetchHouses({ commit }) {
const response = await api.fetchHouses();
commit("setHouses", response.data);
console.log(response.data);
},
createNewHouse(_, formData) {
api.createNewHouse(formData);
},
async deleteHouse(_, house) {
api.deleteHouse(house)
const response = await api.fetchHouses();
commit("setHouses", response.data);
},
async houseDetail({ commit }, id) {
const response = await api.fetchHouses();
response.data.forEach((house) => {
if (house.id === id) {
console.log(house);
commit("setSelectedHouse", house);
}
});
},
};
const mutations = {
setHouses: (state, houses) => {
state.houses = houses;
},
setSelectedHouse: (state, selectedHouse) => {
state.selectedHouse = selectedHouse;
},
};
export default {
state,
getters,
actions,
mutations,
};
here is the store where i have the manage the state of the app, in the deleteHouse action function i delete the house then try to get a new api response and set the state of houses to the new updated state of the houses array.
api
import axios from "axios";
const API_KEY = "xxxxxxxxx";
export default {
fetchHouses() {
return axios.get("https://api.intern.d-tt.nl/api/houses", {
headers: {
"X-Api-Key": API_KEY,
},
});
},
async deleteHouse(id) {
axios
.delete(`https://api.intern.d-tt.nl/api/houses/${id}`, {
headers: {
"X-Api-Key": API_KEY,
},
})
.then(() => {
console.log("successful deletion");
});
},
createNewHouse(formData) {
console.log("api page", formData);
const image = formData.houseImage;
return axios
.post("https://api.intern.d-tt.nl/api/houses", formData.form, {
headers: {
"X-Api-Key": API_KEY,
},
})
.then((res) => {
console.log("REACHED FIRST POST");
const id = res.data.id;
const formData = new FormData();
formData.append("image", image[0]);
axios
.post(
`https://api.intern.d-tt.nl/api/houses/${id}/upload`,
formData,
{
headers: {
"X-Api-Key": API_KEY,
},
}
)
.then(console.log("success"))
.catch((err) => console.log(err));
})
.catch((err) => {
console.log(err);
});
},
};
here is the api file that i use to make all api requests. i hope this information helps.

Per #RenauldC5, you didn't provide enough information about your allHouses getter or where and how the data gets into the store or where setHouses stores the data, and how.
However a question/tips to guide you:
#setHouses - I presume your api returns an array .. (?) So make sure you initialize the property (key) in the store (at whatever key #setHouses stores this array) at instantiation so that the store and Vue instances that use the allHouses getter know to watch that array property
#deleteHouses - When you delete the array, you seem to return a new array from the api. If this is true, simply setting the new non-reactive array overtop of the old reactive one will create an unreactive condition. This is a fundamental understanding of Vue's reactivity system - and is likely the cause of your problem.
Fixes:
Whatever key #setHouses uses to set data on the Vuex store, instantiate it as an empty array when the store is created.
#setHouses must iterate response.data and array.push(item) onto this reactive array, rather than simply replace the reactive array with a new (non-reactive) array.
#deleteHouse - should first use array.splice(0) to remove all children in the reactive array, then setHouses will array.push(child) into this reactive array
Update: including examples
//- update: state:
const state = {
houses: [],
selectedHouse: null,
};
//- update: #setHouses
setHouses: (state, houses) => {
// empty the previous reactive array
state.houses.splice(0);
// push the new houses to the original reactive array
state.houses.push(...houses);
// state.houses now remains bound to your getters, vue instances and remains reactive
},
Update: add examples of changes
//- update:state
const state = {
houses: [],
selectedHouse: null,
};
//- update:#setHouses
setHouses: (state, houses) => {
// empty the previous reactive array
state.houses.splice(0);
// push the new houses to the original reactive array
state.houses.push(...houses);
// state.houses now remains bound to your getters, vue instances and remains reactive
},
PS: maybe I'm not clear on what your action #houseDetail does but it seems to re-load ALL houses ... perhaps this is what you want (?)

Related

VUEX mutate array data each request

I have some data from an API that I am storing in VUEX and then displaying in the UI. On initial page load there is a request that pulls in the initial data and displays. All works well. My issues is When I now submit a form input for another request using an event handler I am just pushing to the array and it is adding to the array (which makes sense) and creates another instance below the current data which I do not want. Is there a way to actually CHANGE / MUTATE the current data that is in the array and update the UI with the new values?
STORE
import { createStore } from 'vuex';
import axios from 'axios';
export default createStore({
state: {
ipData: [],
currentIP: '',
},
mutations: {
SET_CURRENT_IP(state, currentIP) {
state.currentIP = currentIP;
},
SET_IP_DATA(state, ipData) {
state.ipData.push(ipData);
},
},
});
FORM SUBMIT
methods: {
async submitForm() {
const isFormValid = await this.v$.$validate();
if (!isFormValid) return;
axios
.get(`${this.url}${this.getIP}`, {
headers,
})
.then((response) => {
this.$store.commit('SET_IP_DATA', response.data);
})
.catch((error) => {
console.log(error.response);
});
},
},
VUEX OBJECT:
ipData:Array[1]
0:Object
as:Object
domains:Array[5]
ip:"8.8.8.8"
isp:"Google LLC"
location:Object
city:"Mountain View"
country:"US"
geonameId:5375480
lat:37.38605
lng:-122.08385
postalCode:"94035"
region:"California"
timezone:"-07:00"
If your ipData is array of object, you can create another mutation for updating your array (use id or some other identifier to match right object):
UPDATE_IP_DATA(state, payload) {
state.ipData = [
...state.ipData.map((item) =>
item.id !== payload.id
? item
: {
...item,
...payload,
}
),
];
}

Why is state not updating in vuex?

I'm using vuex with axios to fetch data from the backend. It shows the updated state when I console-log the state. However, there is always no data when I call the state. Is there something I've missed?
vuex file
import axios from "axios";
import { uid } from "quasar";
const state = {
data: {},
};
const mutations = {
addData(state, payload) {
state.data[payload.id] = payload.data;
console.log(state.data); //the data exist
},
};
const actions = {
readData({ commit }) {
axios({
method: "get",
url:
"https://dev.activate.vi9e.com/api/purchase/receivegoods/index?action=list",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
for (var i = 0; i < response.data.length - 1; i++) {
let dataId = uid();
let payload = {
id: dataId,
data: response.data[i],
};
commit("addData", payload);
}
})
.catch((error) => {
//handle error
console.log("error message: ", error.message);
});
},
};
const getters = {
data: (state) => {
return state.data;
},
};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};
Here is my template:
<template>
<p>{{ data }}</p>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
methods: {
...mapActions("incominggoods", ["readData"]),
},
computed: {
...mapGetters("incominggoods", ["data"]),
},
mounted() {
this.readData();
}
};
</script>
Even though, I get the data whenever I console-log in mutations, the data in state is not updating at all. How can I fix this?
SOLUTION
Thanks for all the comments. They are all worth problem-solving. The problem is due to the vue's reactivity. Vue seemingly doesn't allow handling the "object" (non-primitive data type) and not allowing the object is assigned by the array assignment way to handle the "Object". This is what I did based on the comments:
I manipulated the data that I received from the backend manually (in which it has the "object data type" (non-primitive data type) and changed it to "Object data type").
In actions (within then(response))
for (var i = 0; i < response.data.length; i++) {
let dataId = uid();
// manipulate the "object" to "Object"
let data = {
poid: response.data[i].purchase_order_id,
podate: response.data[i].po_date,
vendor: response.data[i].vendor_name,
qty: "0%",
total_qty_ordered: response.data[i].total_qty_ordered,
total_qty_receivable: response.data[i].total_qty_receivable,
total_qty_received: response.data[i].total_qty_received,
status: response.data[i].status,
barcode: response.data[i].purchase_order_id,
printed_id: response.data[i].printed_id,
};
// END OF THE MANIPULATION
let payload = {
id: dataId,
data: data,
};
commit("addData", payload);
In mutations
addData(state, payload) {
Vue.set(state.data, payload.id, payload.data);
},
JS reactivity is a bit weird with non-primitive types (objects and arrays, object in further reading).
Imagine an object as a bus with tinted windows. If a passenger (item in an array or key/value in an object) or passengers go in or out of the bus and you were looking at it from the street, you'd have no idea that something changed, the bus still looks the same. That's basically how it works, and you have 2 options for object reactivity.
You can either:
a) replace the whole object
b) in the case of Vue, use Vue.set (for actual objects, not for arrays)
Option a) looks like this:
// for objects
const addObjectProperty = (state, property) => {
state.object = {...state.object, property)
}
// for arrays
const addArrayItem = (state, item) => {
state.array = [...state.array, item]
}
What this is doing, is creating a new object on the fly using object literal syntax (the { } or [ ]). So this way, Vue is looking at your state.object, and see it's replaced with a whole new one and reacts accordingly.
Option b) looks like this:
const addObjectProperty = (state, property) => {
// Vue.set params: object, key, value
Vue.set(state.object, state.object[property], property)
}
As for arrays, just use array methods, and don't manually change values like arr[x] = 10 as that will cause the same issue.
EDIT:
If you have a nested object as a state property, you still have to replace the top-level object.
state: {
this: {},
is: {},
what: {},
you: {},
need: {},
to: {},
change: {},
not: {
this: {}
}
}
}
The easiest way to go about this if you have lots of nested state is by using vuex modules
Vue's reactivity system cannot detect object property addition, see Reactivity in Depth.
In your mutation, change this
state.data[payload.id] = payload.data;
to this:
Vue.set(state.data, payload.id, payload.data);

Vuefire data not subscribing to changes

I have several different queries that I need to keep 'live' data from. When I hardcode the query it successfully shows all live changes that happen in the database. But when I pass a payload the data won't change until reloaded.
Working Query:
getOnline: firestoreAction(({ bindFirestoreRef }) => {
return bindFirestoreRef('online', db.collection('units').where('unit.on', '==', true).orderBy('info.timestamp', 'desc').orderBy('unit.status'))
}),
Not working Payload Query: gets data once
getFilterSystemId: firestoreAction(({ bindFirestoreRef} , payload) => {
return bindFirestoreRef('filterBySystemId', db.collection('units').where('unit.group', '==', payload.group).orderBy('info.timestamp', 'desc').orderBy('unit.status'))
}),
I pass the payload simply:
filterSystemId(grouphex){
let payload = {
group: grouphex.toString(),
}
this.getFilterSystemId(payload);
},
How do I pass a payload AND get live updates to any changes that happen in the database?
Ended up using vuefire instead of vuexfire and dynamically binding my queries like this.
const vuefire = db.collection('vuefire')
export default {
components: {},
data() {
return {
//view
vuefire: [],
id: 'true',
user: [],
code: 'true'
};
},
created() {
},
// firestore: {
// vuefire: db.collection('vuefire')
// }
watch: {
id: {
// call it upon creation too
immediate: true,
handler(id) {
this.$bind('user', vuefire.where('a', '==', id))
},
},
Anytime 'id' changes the dataset ('user') is recomputed and accomplishes my goal

Unwanted state changes in a class component with React

Long story short, I have a class component that constructs a poll. Before sending the data to the server I need to transform it a little so it fits the API request. I created a transformData method on my class component that transforms the data derived from the state. As a side effect it sets the data in separate this.state.data property so I can attach it with the API request. The problem is that the method mutates the other properties of the state.
transformData = () => {
const { title, sections } = this.state
const transformedSections = sections.map(section => {
delete section.isOpen
const transformedQuestions = section.questions.map(question => {
question.label = question.question
question.type = toUpper(question.type)
delete question.question
return question
})
section.questions = {
create: transformedQuestions,
}
return section
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
}
So I get this:
state: {
data: {...} //our transformed data
sections: {...} //transformed as well!!
}
instead of getting this:
state: {
data: {...} //our transformed data
sections: {...} //same before calling the method
I re-wrote the method with different approach — basically replaced all Array.map with Array.forEach and it worked as expected.
transformData = () => {
const { title, sections } = this.state
const transformedSections = []
sections.forEach(section => {
const transformedQuestions = []
section.questions.forEach(question => {
transformedQuestions.push({
label: question.question,
type: toUpper(question.type),
max: question.max,
min: question.min,
instruction: question.instruction,
isRequired: question.isRequired,
placeholder: question.placeholder,
})
})
transformedSections.push({
title: section.title,
questions: { create: transformedQuestions },
})
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
Can anyone explain what's going on here? How can I accidentally mutate a state property without explicitly calling this.setState on the aforementioned property? The thing is that the originally written method mutates the state even if I return the data object without calling this.setState whatsoever. Like so:
//This still mutates the state
return {
data: {
title,
sections: { create: transformedSections },
}
}
//without this!
//this.setState({
// data: {
// title,
// sections: { create: transformedSections },
// },
// })
Thanks!
javascript behave like this way,
its called variable referencing.
it works like pointer variable in C.
if your console those variable such as console.log(var1 == var2) it will show true cuz both references from same memory location
if you want to prevent mutate original variable then you have to create another brand new variable to mutate
like this way :
const { title, sections } = this.state
// create new variable following old one (spreading es6 way)
const tempSections = [...sections]
...
also
sections.forEach(section => {
const transformedQuestions = []
const tempQuestions = [...section.questions]
tempQuestions.forEach(question => {
...
always have to create a brand new variable of object/array/... to prevent auto mutation
for further info here
Issue here is of Shallow Copying :
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
// state is muate already
console.log("---- After map -----" , this.state);
You can run the below code snippet and check both console.log, and check for "isOpen": true
Hope this will clear all your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
state = {
title : "questions" ,
sections : [{
isOpen : true ,
questions : ["que1" , "que2" , "que3"]
}]
}
transfromData = () => {
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
console.log("---- After map -----" , this.state);
}
render() {
return (
<div>
<button onClick={this.transfromData}>transfromData</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
You should never update the state without using the setState method. It is asyncronous, and if you don't set it properly you never know what might happen - and that's what you're seeing in the first part of your answer. See the docs
By doing
section.questions = {
create: transformedQuestions,
}
you are improperly altering the state, so you'll see this.state.sections transformed as well, because each element inside this.state.sections has now an attribute questions that contains create with the value transformedQuestions

Updating Apollo store when mutating list filter

I have a graphql schema that looks like this :
type User {
entries(status: EntryStatus): [Entry]
}
type Mutation {
updateEntry(status: EntryStatus): Entry
}
I can filter the list of entries by status (which is an enum) and I can update the status of an entry. I'd like to update the store when the status is updated so that the entry appears in the right list (entries(status: FOO) to entries(status: BAR)).
I know I can update the store with the update method.
const withMutation = graphql(updateEntryMutation, {
props: ({ mutate }) => ({
updateEntry: updateEntryInput =>
mutate({
variables: { updateEntryInput },
update: (proxy, newData) => {
// update data here
// Which would involve removing the entry from its previous filtered "list", and adding it to the one with the new status
})
})
});
But how can I know from which list to remove the entry since I don't have access to the old data (previous entry.status) from update ?
(apart from enumerating all lists by status and removing the updated entry if I find it...)
You need to store.readQuery() first, then write the new data to the store.
Kinda like this:
const EntriesQuery = 'yourQueryHere';
const withMutation = graphql(updateEntryMutation, {
props: ({ mutate }) => ({
updateEntry: updateEntryInput =>
mutate({
variables: { updateEntryInput },
update: (store, { data: { updateEntry }}) => {
const data = store.readQuery({ query: EntriesQuery });
data.entries.push(updateEntry)
store.writeQuery({ query: EntriesQuery, data })
})
})
});

Categories