Why is state not updating in vuex? - javascript

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);

Related

Vue component is not reactive

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 (?)

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,
}
),
];
}

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

Redux state updates two seperate objects

I have influencer data object. This object is beeing pulled from database with action FETCH_INFLUENCER and put inside two different objects: influencer and formInfluencer in redux store. And then I have action SET_INFLUENCER that is supposed to create new instance of the state and update influencer object in redux. For some reason though it updates both influencer and formInfluencer. I really struggle with finding answer here since I think I did everything to prevent pointing of two different variables to the same object and still it happens.
reducer:
case 'FETCH_INFLUENCER_FULFILLED':
return { ...state, fetching: false, fetched: true, influencer: action.payload.data, formInfluencer: Object.assign([], action.payload.data) }
case 'SET_INFLUENCER':
return { ...state, influencer: action.payload }
actions:
export function fetchInfluencer(id) {
return {
type: "FETCH_INFLUENCER",
payload: axios.get('/api/influencer/' + id, {headers: {Authorization: 'Bearer ' + localStorage.getItem('token')}})
}
}
export function setInfluencer(influencer) {
return {
type: "SET_INFLUENCER",
payload: influencer
}
}
dispatch:
handleUserChange(e) {
let influencer = [...this.props.influencer]
influencer[0].user[e.target.name] = e.target.value;
this.props.dispatch(setInfluencer(influencer))
}
mapping state to props:
const mapStateToProps = (state) => {
return {
influencer: state.influencers.influencer,
formInfluencer: state.influencers.formInfluencer
}
}
export default connect(mapStateToProps)(InfluencerDetails)
If You have any idea why this could be happening I would be happy to hear the answer.
You shouldn't mutate state (if you don't mutate it, then it is no problem that you have multiple variables pointing to the same object).
Instead of:
handleUserChange(e) {
let influencer = [...this.props.influencer]
influencer[0].user[e.target.name] = e.target.value;
this.props.dispatch(setInfluencer(influencer))
}
You should do a bit more work:
handleUserChange(e) {
const newUser = {
...this.props.influencer[0].user,
[e.target.name]: e.target.value
};
const newInfluencer = {
...this.props.influencer[0],
user: newUser
};
const newInfluencers = [...this.props.influencer];
newInfluencers[0] = newInfluencer;
this.props.dispatch(setInfluencer(newInfluencers));
}

My mutations aren't working! How do I correct this?

I am trying to set the breakfastMenu array in state as shown below but I can't see the array being filled in my vue-devtools.
I have properly set-up the Vuex methods and checked twice, also I didn't receive any sort of error. So, why do I have a logical error in my code?
store.js:
export default new Vuex.Store({
state: {
menu: [],
breakfastMenu: [],
lunchMenu: [],
dinnerMenu: []
},
mutations: {
'SET_MENU': (state, menuMaster) => {
state.menu = menuMaster;
},
'SET_BREAKFAST_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_LUNCH_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_DINNER_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
},
actions: {
initMenu: ({ commit }, menuMaster) => {
commit('SET_MENU', menuMaster)
},
initBreakfastMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 1) {
commit('SET_BREAKFAST_MENU', element)
}
});
},
initLunchMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 2) {
commit('SET_LUNCH_MENU', element)
}
});
},
initDinnerMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 3) {
commit('SET_DINNER_MENU', element)
}
});
},
},
getters: {
getBreakfastMenu(state) {
return state.breakfastMenu
},
getLunchMenu(state) {
return state.lunchMenu
},
getDinnerMenu(state) {
return state.dinnerMenu
},
}
})
Breakfast.vue:
import { mapActions, mapGetters } from 'vuex';
export default {
data() {
return {
breakfastArray: []
};
},
methods: {
...mapActions(['initBreakfastMenu']),
...mapGetters(['getBreakfastMenu']),
},
created() {
this.initBreakfastMenu;
this.breakfastArray = this.getBreakfastMenu;
}
};
No error messages so far!
I need the breakfastMenu array filled in store.js.
Please help out!
A few thoughts.
Firstly, this line:
this.initBreakfastMenu;
You aren't actually calling the method. It should be:
this.initBreakfastMenu();
Next problem is this:
...mapGetters(['getBreakfastMenu']),
The line itself is fine but it's inside your methods. It should be in the computed section.
You haven't included any sample data for state.menu but it's also worth noting that initBreakfastMenu won't do anything unless there is suitable data inside state.menu. I suggest adding some console logging to ensure that everything is working as expected there.
SET_BREAKFAST_MENU, SET_LUNCH_MENU and SET_DINNER_MENU are all modifying state.breakfastMenu. I would assume that this is incorrect and each should be modifying their respective menu.
I would also note that using local data for breakfastArray is suspicious. Generally you'd just want to use the store state directly via the computed property rather than referencing it via local data. This is not necessarily wrong, you may want to detach the component data from the store in this way, but keep in mind that both are referencing the same array so modification to one will also affect the other. You aren't taking a copy of the array, you're just creating a local reference to it.
You should also consider whether you actually need the 4 menu types within your state. If breakfastMenu, lunchMenu and dinnerMenu are all just derived from menu then you'd be better off just implementing those using getters. getters are the store equivalent of computed properties and can contain the relevant filtering logic to generate their value from state.menu.
initBreakfastMenu is an action and you may want to use this.initBreakfastMenu()

Categories