React's setState mutates variable - reactjs

Very strange behavior, most probably a bug in my code:
I get an API response object (json) and when I setState using one of the object's properties as value, the object changes. I thought this was a problem with the backend or fetch method, but it has been proven not to be.
Code is generally as follows:
fetch(this.props.url, {
method: 'POST',
body: JSON.stringify({
class: 'property',
action: 'view',
objectId: this.props.element.id,
token: this.props.token,
}),
})
.then(response => response.json())
.then(json => {
this.setState({
property: json.property
});
console.log(json);
})
What I am supposed to get:
{
property: {
units: {
{
id: 31,
...
},
{
id: 33,
...
}
}
}
}
{
What I actually get:
property: {
units: {
{
id: 33,
...
},
{
id: 33,
...
}
}
}
}
Needless to say, response from backend is with the proper ids, which are unique.
Any ideas what I might have done wrong? This is not supposed to happen, is it? How does the json variable change?
Thanks.

Ok, found my mistake. It is very confusing and honestly, I believe this should not happen by design, but anyways:
Once I set state.property to json.property, I pass the state.property as a prop to a child element. Within that child element I am doing some filtering and mapping using props.property and at one point I've mistakenly used a single equal (=) sign instead of double (==) when comparing the ids in a filter function.
Apparently this sets the wrong id and goes all the way back to the initial json response (so, filter function -> child element props -> ?parent element state? -> json response). So, be ccareful with your filter functions, unless you want to spend a few days tracking the impossible.

Related

How to iterate an object retrieved by Fetch API in VueJS?

I have a data Object resulting by Fetch:
data () {
return {
show: true,
interval: {},
dadosCliente: {},
radio: ''
}
fetchNome() {
fetch('http://localhost:3000/pessoas/'+this.$store.state.cpf)
.then(response => response.json())
.then(data => {
this.dadosCliente = data
this.show = false;
});
}
Output dadosCliente:
[{'nome':'Paulo'},{'nome':'Marcos'},{'nome':'Victor'}]
When I try to iterate v-for in v-radio (vuetify) I receive message saying which item is not declared.
<v-radio-group v-model="radio" mandatory>
<v-radio :v-for="item in dadosCliente" :key="item.nome" :label="`${item.nome}`"></v-radio>
</v-radio-group>
Speding a time reading some articles, posts, but I couldnt finish it. Anyone can help me ?
You shouldn't use binding sign : with directives like v-if or v-for it should be like:
v-for="item in dadosCliente"
Removing the : will make it work...
To add on ..since you are iterating an array, instead of binding the nome to the key, you can do something like this
<v-radio-group v-model="radio" mandatory>
<v-radio :v-for="(item,index) in dadosCliente" :key="index" :label="`${item.nome}`"></v-radio>
</v-radio-group>
In this way, you can even handle objects with redundant nome values.

Adding objects fetched from local restify server into object in state

So I'm taking a course in web programming and in it we've gotten this assignment to design some simple front end for ordering salads, to get all the components etc. it was previously stored in a .js file in the following fashion
let inventory = {
Sallad: {price: 10, foundation: true, vegan: true},
Pasta: {price: 10, foundation: true, gluten: true},
'Salad + Pasta': {price: 10, foundation: true, gluten: true},
'Salad + Matvete': {price: 10, foundation: true, vegan: true, gluten: true},
'Kycklingfilé': {price: 10, protein: true},
'Rökt kalkonfilé': {price: 10, protein: true},
'Böngroddar': {price: 5, extra: true, vegan: true},
'Chèvreost': {price: 15, extra: true, lactose: true},
Honungsdijon: {price: 5, dressing: true, vegan: true},
Kimchimayo: {price: 5, dressing: true},
.
.
.
};
export default inventory;
This is then imported into my App.js that was created when creating the react project and sent as a prop to another component that took care of the composing of a salad that was eventually sent back to a function also sent with as a prop.
So what we're supposed to do now is to get this inventory from a local rest(?) server instead. So if I go to
http://localhost:8080/proteins
it will open a page that just displays an array with all the different choices of proteins
["Kycklingfilé","Rökt kalkonfilé","Norsk fjordlax","Handskalade räkor från Smögen","Pulled beef från Sverige","Marinerad bönmix"]
And then going to
http://localhost:8080/proteins/Kycklingfilé
Will give you another page with the properties of that ingredient
{"price":10,"protein":true}
And my attempt at recreating that inventory object with all the ingredients as properties inside state is this
class App extends Component {
constructor(props) {
super(props);
this.state = {
salads: [],
inventory: {
}
};
}
componentDidMount() {
const base = "http://localhost:8080/";
const pURL = base + "proteins/";
const fURL = base + "foundations/";
const eURL = base + "extras/";
const dURL = base + "dressings/";
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {e : data})
})
})
});
fetch(pURL).then(response => response.json()).then(data => this.setState({data}));
fetch(eURL).then(response => response.json()).then(data => this.setState({data}));
fetch(dURL).then(response => response.json()).then(data => this.setState({data}));
}
I've been using
{JSON.stringify(this.state)}
to try and look at whats going on and with this code it comes out as this
{"salads":[],"inventory":{},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So the fetch works fine for getting all the ingredients of a certain type, I guess it's only the dressings since it overwrites data each time on those last three fetches. But the problem is that inventory is completely empty.
If I instead write it like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: fetch(fURL + e).then(response => response.json().then())})
})
});
The output becomes
{"salads":[],"inventory":{"e":{}},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it adds the 'e' object, which is another problem since I want it to be the value of the current element, but it's completely empty, and I dont know how to get the data from that seconds fetch when I write it like that. So that's why it now looks like it does in the first code snippet, where it doesn't even get an empty 'e' inside inventory.
Finally, if I write it like that second example but just e: e like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: e})
})
});
The output becomes
{"salads":[],"inventory":{"e":"Salad + Quinoa"},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it seems like everything is working up until the .forEach on the array of strings that represents a certain type of ingredient since it manages to put that into 'e' inside inventory with one of the array elements as it's value. It's only the last one in the list though but I guess that stems from the problem that it just makes the object 'e' and not the value of the current element and overwrites it for every item.
Sorry if all the rambling made the problem unclear, but what I'm trying to achieve is inventory {} inside state that looks like it did when it was in a seperate file, so that when we create the component we can send this.state.inventory instead of the imported inventory as prop. And to create that using what we can fetch from the different pages.
When you write
{e : data}
you create a new Object with a single entry. That sets the value of the key 'e' as the current value of the variable 'data'. A variable named 'e' is not involved:
const e = 'hello';
console.log(e); // "hello"
console.log({ asd: e }); // { asd: "hello" }
console.log({ e: "asd" }); // { e: "asd" }
console.log({ e: asd }); // ReferenceError: asd is not defined
What you are trying to do is using the value of the variable e as the key that you want to set. In javascript this is done using [ and ] like so:
const e = 'hello';
console.log({ [e]: "world" }); // { hello: "world" }
// this is necessery whenever you want a key that is not a simple word
console.log({ ["key with spaces"]: "world" }); // { "key with spaces": "world" }
console.log({ [e + e]: "world" }); // { hellohello: "world" }
EDIT:
there is another issue with your code above that you might encounter sooner or later:
In React you should never ever modify this.state directly. Always go through this.setState()!
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
In your case this is a bit more difficult, since you are making multiple requests which each affect the same key in your state (inventory).
Because you cannot know in what order the requests arrive, and whether React will actually do the setState each time new data comes, or do them all at the same time, you cannot simply use this.setState({ inventory: newInventory }). Instead you should use the function version as described here. Unfortunately this can be a bit complex to grasp in the beginning :(
in your case I would solve it like this:
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e)
.then(response => response.json())
.then(data => this.setState((prevState) => ({
inventory: Object.assign({}, prevState.inventory, {[e]: data}),
})));
})
})
});
A couple of things to note here:
note the ({ in (prevState) => ({ ... }): this is an arrow function that returns an object
we are passing a function to this.setState (see the link above for details). This function receives the current state as an argument (prevState) and should return the new State. (although it can omit keys of the old state that remain unchanged). This is better than directly passing the new state to this.setState because when multiple setState happen at the same time, React can apply the functions you pass in the right order so that all changes happen, but if you passed objects it has to decide on one of them to 'win' so changes can get lost.
In Object.assign({}, prevState.inventory, {[e]: data}), instead of modifying prevState.inventory we create a new object that contains the updated inventory. You should never modify the old state, even in this.setState.
Hope this helps :)
So with #sol's advice to use [e] to create the objects for each ingredient, this code
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + [e]).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {[e] : data})
})
})
});
now works. I think why it didn't look successful with my "troubleshooting" of just printing that JSON.stringify of the entire state in render was that is just didn't render properly when react refreshed after saving the code. Updating the page makes it all blank, but clicking onto another page through a link and then back fixes it. Dont know why, but I'll take it.

How to fetch value from array which came from json in Angular 5 app?

I have the following JSON definitions:
export class Company {
name: string;
trips : Trip[] = [];
}
export class Trip{
id: number;
name: string;
}
I am able to see the trips in the console using:
console.log(this.company);
In the component I have the following method:
if(this.company) {
Object.keys(this.company.trips).forEach((data) => {
console.log(data);
});
}
What I am getting in the console is the trip's properties names which is "id" and "number".
I would like to know how to access the value.
According to your data structure, you should not even try to do Object.keys what you should be doing as the Trip is an object, is something like following in which you treat the iteration object as a real Trip object
if(this.company && this.company.trips){
this.company.trips.forEach((trip:Trip) => {
console.log(trip.id + '\n' + trip.name);
});
}
if you have any issue with this piece of cod then make sure you are correctly doing your deserialization process and objects are getting cast properly.

How do we partially update a state in react?

I was wondering what is the best way to partially update a state of a component in React/React-Native. Other than the fact that I can make a function which takes the current state and creates a new state and merges the new {key:value} and the previous state. For example:
{
dataStream:[//having data here],
formData: {
'first_name': 'Richard',
'last_name' : 'Barbieri',
}
}
I want to update last_name to another value. When I call
this.setState(formData:{{'last_name':newValue}}), it resets the formData dictionary to just last name: new Value. Is there a way to this efficiently?
I think there are two things you could try:
Spread operator
liks so
this.setState({
formData: {
...this.state.formData,
"last_name" : newValue
}
});
or
Take current state's first_name and reapply it:
like so
this.setState({
formData: {
"first_name": this.state.formData.first_name,
"last_name" : newValue
}
})
I'm not too sure about the first one, but I think the second one should work.
What happens is normal because you reassign the whole forData.
If you want to add something to the existing form data do something like that (there are plenty of other solutions ^^)
this.setState({
formData: Object.assign(this.state.formData, { 'last_name': newValue }
})

React-Redux update state of immutable object with nested array, delete element from array

I have the following object, from which I want to remove one comment.
msgComments = {
comments: [
{ comment:"2",
id:"0b363677-a291-4e5c-8269-b7d760394939",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" },
{ comment:"1",
id:"e88f009e-713d-4748-b8e8-69d79698f072",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" }
],
email:"test#email.com",
id:"e93863eb-aa62-452d-bf38-5514d72aff39",
post:"test",
title:"test"
}
The action creator hits the api delete function with the commentId:
// DELETE COMMENT FROM POST
export function deleteComment(commentId) {
return function(dispatch) {
axios.post(`${API_URL}/datacommentdelete`, {
commentId
},{
headers: { authorization: localStorage.getItem('token') }
})
.then(result => {
dispatch({
type: DELETE_COMMENT,
payload: commentId
});
})
}
}
My api deletes the comment and I send the comment id to my Reducer, this is working fine to this point, api works and comment is deleted. The problem is updating the state in the reducer. After much trial and error at the moment I am trying this.
case DELETE_COMMENT:
console.log('State In', state.msgComments);
const msgCommentsOne = state.msgComments;
const msgCommentsTwo = state.msgComments;
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload );
const newComments = [
...msgCommentsTwo.data.comments.slice(0, deleteIndexComment),
...msgCommentsTwo.data.comments.slice(deleteIndexComment + 1)
];
msgCommentsOne.data.comments = newComments;
console.log('State Out', msgCommentsOne);
return {...state, msgComments: msgCommentsOne};
Both state in AND state out return the same object, which has the appropriate comment deleted which I find puzzling.
Also the component is not updating (when I refresh the comment is gone as a new api call is made to return the updated post.
Everything else seems to work fine, the problem seems to be in the reducer.
I have read the other posts on immutability that were relevant and I am still unable to work out a solution. I have also researched and found the immutability.js library but before I learn how to use that I wanted to find a solution (perhaps the hard way, but I want to understand how this works!).
First working solution
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
email: state.msgComments.data.email,
post: state.msgComments.data.post,
title: state.msgComments.data.title,
id: state.msgComments.data.id,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
Edit:
Second working solution
I have found a second far more terse solution, comments welcome:
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
...state.msgComments.data,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
That code appears to be directly mutating the state object. You've created a new array that has the deleted item filtered out, but you're then directly assigning the new array to msgCommentsOne.data.comments. The data field is the same one that was already in the state, so you've directly modified it. To correctly update data immutably, you need to create a new comments array, a new data object containing the comments, a new msgComments object containing the data, and a new state object containing msgComments. All the way up the chain :)
The Redux FAQ does give a bit more information on this topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
I have a number of links to articles talking about managing plain Javascript data immutably, over at https://github.com/markerikson/react-redux-links/blob/master/immutable-data.md . Also, there's a variety of utility libraries that can help abstract the process of doing these nested updates immutably, which I have listed at https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md.

Resources