undefined on getting specific value from State array - React Native - javascript

I'm reading data from firestore and stores it in state array of objects.
when i
console.log(this.state.array)
it returns the whole array with all the data of the objects, but when i
console.log(this.state.array.name)
or
console.log(this.state.array[0])
it returns undefined
.
I have tried to get the data with
forEach
loop but it seems to be not working as well.
constructor(props) {
super(props);
this.state = { tips: [] };
}
componentDidMount() {
firebase.firestore().collection('pendingtips').get()
.then(doc => {
doc.forEach(tip => {
this.setState([...tips], tip.data());
console.log(this.state.tips);
});
})
.catch(() => Alert.alert('error'));
}
renderTips() {
console.log(this.state.tips); //returns the whole array as expected
console.log(this.state.tips[0].name); //returns undefined
return this.state.tips.map(tip => <PendingTip key={tip.tip} name={tip.name} tip={tip.tip} />); //return null because tip is undefined
}
render() {
return (
<View style={styles.containerStyle}>
<ScrollView style={styles.tipsContainerStyle}>
{this.renderTips()}
</ScrollView>
</View>
);
}
the array structure is:
"tips": [
{ name: "X", tip: "Y" },
{ name: "Z", tip: "T" }
]
so I expect this.state.tips[0].name will be "X" instead of undefined.
thanks in advance.

First of all you should fetch data in componentDidMount instead of componentWillMount.
https://reactjs.org/docs/faq-ajax.html#where-in-the-component-lifecycle-should-i-make-an-ajax-call
Secondly, you should use this.setState to update your state, instead of mutating it directly.
componentDidMount() {
firebase
.firestore()
.collection("pendingtips")
.get()
.then(docs => {
const tips = docs.map(doc => doc.data());
this.setState({ tips });
})
.catch(() => Alert.alert("error"));
}

I Found out that the problem was that JavaScript saves arrays as objects.
for example this array:
[ 'a' , 'b' , 'c' ]
is equal to:
{
0: 'a',
1: 'b',
2: 'c',
length: 3
}
"You get undefined when you try to access the array value at index 0, but it’s not that the value undefined is stored at index 0, it’s that the default behavior in JavaScript is to return undefined if you try to access the value of an object for a key that does not exist."
as written in this article

firesore requests are async, so by time your request gets execute your component is getting mounted and in a result you are getting undefined for your state in console.
You must do API call in componentDidMount instead of componentWillMount.
Mutating/changing state like this, will not trigger re-render of component and your component will not get latest data,
doc.forEach(tip => {
this.state.tips.push(tip.data());
console.log(this.state.tips);
});
You must use setState to change your state, doing this your component will get re-render and you have latest data all the time.
componentDidMount(){
firebase.firestore().collection('pendingtips').get()
.then(doc => {
const tipsData = doc.map(tip => tip.data());
this.setState({tips:tipsData},() => console.log(this.state.tips));
})
.catch(() => Alert.alert('error'));
}
While calling renderTips function make sure your state array has data,
{this.state.tips.length > 0 && this.renderTips()}

Related

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

Why is React fetch ignoring the JSON data I'm getting?

I am teaching myself how to "fetch" data within React. However, although I am able to grab the JSON data from a local file, it somehow disappears when I try to place it into the component's state.
I am using the following tutorial. The precise code I'm copying is the second image at this link:
https://www.robinwieruch.de/react-fetching-data/#react-how-fetch-data
For example, the below code, in the log, returns the correct data:
constructor(props) {
super(props);
this.state = { cardData: [] };
}
componentDidMount() {
fetch(cardDataJsonLocation)
.then(response => response.json())
.then(data => console.log(data.cardData));
}
When I look at the console in my browser, the correct data is being logged - an array with 2 objects in it:

[{…}, {…}] 0: {name: "dom", id: 1} 1: {name: "dave", id: 2}
length: 2 __proto__: Array(0)
However, when I change the above code to actually place the above array data in my component's state:
componentDidMount() {
fetch(cardDataJsonLocation)
.then(response => response.json())
.then(data => this.setState({ cardData: data.cardData }))
.then(console.log(this.state));
}
When I log the state, there's no change from the original state I set in the constructor.
In this case, it logs an empty array. If I set the state (in the constructor) to, say, [1,2,3] or null, then that value comes down instead.
Am I missing a step in the fetch process? It's as if it skips the step where I try to setState after fetch. Thanks.
As others have pointed and as described in the documentation, setState() is an async method.
If you need to access the state immediately after calling setState, then put your code in a callback and pass this callback as the second parameter of setState, like in:
this.setState({ cardData: data.cardData }, () => { console.log(this.state); })

Accessing JavaScript Array returns "undefined"

I'm building a simple app in pure Reactjs. Component I'm having problems is a component that is supposed to render a number of buttons by mapping an array that has previously been populated by fetching some data from an external API. This array is populated within a class method and the results are eventually copied onto another array which is part of the state of the component
When I console.log the contents of the array on the render method of my component, everything looks fine. However if I try to print a specific element by its index, "undefined" is printed on the console. As a result the map function does not render all the desired buttons.
I have managed to find different documentation around the way I'm populating the array but none of the articles so far suggest that I'm doing anything fundamentally wrong. At least not that I can see.
State stores an empty array to start with and within the componentWillMount method an API gets called that fetches data and updates the array as per the below:
this.state = {
resources: []
}
getAPIavaiableResources(api_resource) {
let buttonsArray = []
fetch(api_resource)
.then(response => response.json())
.then(data => {
for (let i in data) {
buttonsArray.push({id: i, name: i, url: data[i]})
}
}).catch(error => console.log(error))
this.setState({resources: buttonsArray})
}
componentWillMount() {
this.getAPIavaiableResources(ROOT_RESOURCE)
}
render() {
const { resources } = this.state;
console.log(resources)
console.log(resources[0])
return (
<div className="buttons-wrapper">
{
resources.map(resource => {
return <Button
key={resource.id}
text={resource.name}
onClick={this.handleClick}
/>
})
}
</div>
)
}
This is what gets printed onto the console on the render method.
[]
0: {id: "people", name: "people", url: "https://swapi.co/api/people/"}
1: {id: "planets", name: "planets", url: "https://swapi.co/api/planets/"}
2: {id: "films", name: "films", url: "https://swapi.co/api/films/"}
3: {id: "species", name: "species", url: "https://swapi.co/api/species/"}
4: {id: "vehicles", name: "vehicles", url: "https://swapi.co/api/vehicles/"}
5: {id: "starships", name: "starships", url: "https://swapi.co/api/starships/"}
length: 6
__proto__: Array(0)
Can anyone see what I'm doing wrong? I'm pushing an object because I do want an array of objects albeit arrays in Javascript are objects too. Any help would be appreciated.
Your current implementation is setting state before you have the data, and then mutating state once the api call comes back. React can't tell when you mutate things, and thus doesn't know to rerender. Only when you call setState (or when it receives new props) does it know to rerender.
Instead, wait until you have the data and only then call setState with the populated array.
getAPIavaiableResources(api_resource) {
fetch(api_resource)
.then(response => response.json())
.then(data => {
let buttonsArray = []
for (let i in data) {
buttonsArray.push({id: i, name: i, url: data[i]})
}
this.setState({resources: buttonsArray})
}).catch(error => console.log(error))
}
componentDidMount() {
this.getAPIavaiableResources(ROOT_RESOURCE)
}
The above example also updates the code to use componentDidMount instead of componentWillMount. componentWillMount is deprecated, and wasn't intended for this sort of case anyway.
Currently you are setting the state without waiting for the promise to be resolved. In order to do that, move this.setState({resources: buttonsArray}) after for loop.
In addition, you can render the component conditionally until the you get what you want from the remote resource by doing:
render () {
const { resources } = this.state;
return resources.length
? (
<div>Your content...</div>
)
: null // or some loader
}

Unhandled Rejection (TypeError): Invalid attempt to destructure non-iterable instance

I am trying to learn the map method. If I use this syntax response.data.map(d => I am able to iterate data array and see the results, but if I use this syntax response.data.map(([label, CustomStep]) => {, I am getting the error below:
Unhandled Rejection (TypeError): Invalid attempt to destructure non-iterable instance
Can you tell me how to fix it, so that in future I will fix it myself?
Providing my code snippet below:
axios
.get('http://world/sports/values')
.then(response => {
console.log("sports--->", response.data.map(d => d.customFieldValueName));
//this.setState({ playerRanks: response.data.map(d => d.customFieldValueName) });
// es6 map
//Unhandled Rejection (TypeError): Invalid attempt to destructure non-iterable instance
this.setState({
playerRanks: response.data.map(([label, CustomStep]) => {
label.customFieldValueName
})
})
})
update 1:
hey, I saw in console, data is an array inside that there are so many objects
data: Array(19)
[
{
"customFieldValueCode": "player1",
"customFieldValueName": "player1",
"isActive": "Y"
},
{
"customFieldValueCode": "player 2",
"customFieldValueName": "player 2",
"isActive": "Y"
}
]
EDIT:
Based off the data structure provided you could modify your code to...
axios
.get('http://world/sports/values')
.then(response => {
this.setState({
playerRanks: response.data.map(obj => {
return obj.customFieldValueName
})
})
})
OR
...
response.data.map(({customFieldValueName}) => {
return customFieldValueName;
})
...
OR even...
...
response.data.map(({customFieldValueName}) => customFieldValueName)
...
But this would be my recommended solution to provide type checking on you data and proper error handling...
axios
.get('http://world/sports/values')
.catch(err=> console.log(err))
.then(({data}) => { // Axios always returns an Object, so I can safely 'attempt' to destructure 'data' property
if (data && data.length) { // making sure 'data' does exist, it is an Array and has > 0 elements
this.setState({
playerRanks: data.map(obj => { // Not destructuring here in case obj isn't actually an Object
if (obj && obj.customFieldValueName) return customFieldValueName;
return null;
}).filter(elem=> elem) // BIG-O notation: This sequence is O(2N), as in iterates over the entire Array first with .map(), then iterates over the entire Array again with .filter() to clear out 'null' values
})
}
})
In order to prevent your returned Array above from having a bunch of null elements when they don't conform to our assertions, you can use an Array.reduce() method to 'filter' out any nulls...
axios
.get('http://world/sports/values')
.catch(err=> console.log(err))
.then(({data}) => { // Axios always returns an Object, so I can safely 'attempt' to destructure 'data' property
if (data && data.length) { // making sure 'data' does exist, it is an Array and has > 0 elements
this.setState({
playerRanks: data.reduce((acc,obj) => { // Not destructuring here in case obj isn't actually an Object
if (!obj || !obj.customFieldValueName) return acc; // If it doesn't meet assertions just return the existing accumulator (don't add another element .ie 'null')
return [
...acc, // If it conforms to the assertions the return a new accumulator, by first spreading in all existing elements and the adding the new one (customFieldValueName)
customFieldValueName
]
},[]) // BIG-O notation: This is O(1N) or O(N), as in it will only iterate over the Array one time and the reduce() function will filter out 'null' values at the same time
})
}
})
NOTE:
I also just added .filter(elem=> elem) to the end of my first example, which does the same thing as the new .reduce() functionality, but does this in 1N not 2N operations.
PRE-logged data
Here's how the Array.map() method works...
[1,2].map(element=> {
// element === 1, first iteration,
// element === 2, second iteration
})
Here's how Array destructuring works...
[one, two, ...theRest] = [1,2,3,4,5]
// one === 1 and two === 2 and theRest = [3,4,5]
Here's how Object destructuring works...
{one, three, ...theRest} = {one: 1, two: 2, three: 3, four: 4, five: 5}
// one === 1 and three === 3 and theRest === {two: 2, four: 4, five: 5}
// notice order doesn't matter here (three vs two), but you need to access valid properties from the object you're deetructuring from
So based on the way you function is structured you are making the assumption that the data structure of response.data is...
response.data === [
[
{ customFieldValueName: 'any value' }, // label
{} // CustomStep (this could be any value, not necessarily an Object)
],
[
{ customFieldValueName: 'any value' }, // label
'any value' // CustomStep
]
]
I hope this helps conceptually, but if you'd like a workable solution we will need...
Data structure of response.data. Can you provide result of console.log( JSON.stringify( response.data, null, 5) )
Specific values you are trying to assign to the new this.state.playerRanks Array.
PS: A good way to see Object destructuring in action with your current code is to change...
.then( response => {
To
.then( ({data}) => {
In this case, you should be certain that response.data is an array of arrays, because for each iteration of response.data.map, the function you are providing to the map must receive an array to be able to successfully pull the label and CustomStep values, due to the syntax with which you are destructuring the function parameter.
Imagine data in the following example is the response.data and the parseData function is the function you are passing to the map:
let data = [
[{ customFieldValueName: 'field name' }, { stepData: {} }],
[{ customFieldValueName: 'another field name' }, { stepData: {} }]
];
let parseData = ([label, CustomStep]) => console.log(label.customFieldValueName);
parseData(data[0]); // prints out 'field name'
Otherwise, if response.data is an array of objects, which it seems like it is due to you successfully being able to run response.data.map(d => d.customFieldValueName), you could update your map to this (if you simply want to pull the customFieldValueName value out of the object):
response.data.map(({ customFieldValueName }) => customFieldValueName)

How to update certain object in state array based on received key from DOM

I am confused about executing spread operator and using it to update state array like that
todos: [
{
id: "1",
description: "Run",
completed: "true"
},
{
id: "2",
description: "Pick John",
completed: "false"
}]
I have objects inside my array, the examples provided after searching are using spread operator to update arrays with single object, how can I update that object with "id" that equals "key" only. My wrong function is
markTaskCompleted(e) {
var key = e._targetInst.key;
this.setState(
{
todoList: // state.todoList is todos variable
[...this.state.todoList, this.state.todoList.map(task => {
if (task.id == key) task.completed = "true";
}) ]
},
this.getTodos
);
}
The result of this array is the same array of todos (spread operator) with array of undefined items.
I have been googling for some time but couldn't really get it.
Instead of destructuring the array and using map, I typically update a single item's value with a single map that replaces the item I am updating and returns the existing value for all other items. Something like this:
this.setState((prevState) => {
return {
todoList: prevState.todoList.map((task) => {
if (task.id === key) {
return { ...task, completed: true };
} else {
return task;
}
}),
};
});
Also, notice that this example passes a function to this.setState rather than an object. If you are updating the state based on the previous state (in this example using todoList from the previous state) you should use the function method. setState is asynchronous and you could get unexpected results from using this.state to compute the new state.

Categories