Accessing JavaScript Array returns "undefined" - javascript

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
}

Related

re-populate a directive array reactively in Vue.js

I have an array defined in my data() which gets populated through a custom directive in its bind hook as below:
import Vue from 'vue'
export default {
el: '#showingFilters',
name: "Filters",
data() {
return {
country: '' // v-modelled to a <select>
states: [],
}
},
directives: {
arraysetter: {
bind: function(el, binding, vnode) {
vnode.context[binding.arg] = Object.keys(el.options).map(op => el.options[op].value);
},
},
},
methods: {
countryChangeHandler() {
this.states.splice(0)
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
res.states.forEach( (element,i) => {
Vue.set(this.states, i, element.urlid)
});
})
},
}
The problem starts when I want to re-populate the states array in the countryChangeHandler() method (when #change happens for the country select tag).
I used splice(0) to make the array empty first and I have then used Vue.set to make the re-population reactive, but Vue still doesn't know about it!!! The array has the correct elements though! I just don't know how to make this reactive.
PS: I searched to do this without forEach but $set needs an index.
I'd appreciate any help here.
This solution should work and maintain reactivity.
You should be able to replace the entire array without using splice or set.
I have used a closure to capture this because sometimes the fetch call interferes with the this reference, even inside a lambda expression.
countryChangeHandler() {
this.states = []
const that = this
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
that.states = res.states.map(it=>it.urlid)
})
},

undefined on getting specific value from State array - React Native

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

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

Pushing responses of axios request into array

I have been pounding my head against this problem, and need help with a solution. I have an array of IDs within a JSON, and I am iterating over that array and making a GET request of each ID. I want to then push the response of those GET requests into an array.
Here is the function I am using to push the registrations into the array. It is iterating through an array of IDs:
getRegistrations = async (event) => {
let registrations = [];
await event.registrations.forEach(registration => axios.get('/event/getRegistration', {
params: {
id: registration
}
}).then(res => {
registrations.push(res.data.properties)
}
).catch(err => console.log(err)));
return registrations;
};
Here is where I am calling that code:
render() {
let event = this.getEvent();
let registrations2 = [{
age: 19,
bio: 'test',
firstName: 'hello',
lastName: 'bye',
password: 'adadas',
telephone: "4920210213"
}];
if (this.props.listOfEvents.events.length !== 0 && !this.props.listOfEvents.gettingList && event) { //check if the array is empty and list has not been rendered yet
let columns = [];
let registrations = this.getRegistrations(event);
console.log(registrations);
let eventProperties = event.properties[0];
Object.keys(eventProperties).forEach(key => columns.push({
title: eventProperties[key].title,
dataIndex: key,
key: key
}));
console.log(registrations);
console.log(registrations2);
return (
<h1>hi</h1>
)
}
return <Loading/>
}
When I console-log 'registrations' vs 'registrations2' they should be very identical. However, in the javascript console on Google Chrome, 'registrations appears as '[]' where 'registrations2' appears as '[{...}]'.
I know that it is an issue related to promises (I am returning the registrations array before actually pushing) but I have no idea how to fix it! Some friendly help would be very much appreciated!
I recommend Promise.all, it will resolve single Promise after all promises have resolved. And technically async function is also promise so it will return promise.
here the example.
https://codesandbox.io/s/jzz1ko5l73?fontsize=14
You need to use componentDidMount()lifecycle method for proper execution and state to store the data.
constructor (props) {
super(props);
this.state = {registrations :[]}
}
componentDidMount () {
let response = this.getRegistrations()
this.setState({registrations : response});
}
Then access that state in render method. It's not good practice to call api from render mothod.
Since getRegistrations(event) returns a promise, you should perform operations on its return value inside then.
Instead of
let registrations = this.getRegistrations(event);
console.log(registrations);
Do this
this.getRegistrations(event).then(registrations => {
console.log(registrations);
// other operations on registrations
});

How do you use `reselect` to memoize an array?

Suppose I have a redux store with this state structure:
{
items: {
"id1" : {
foo: "foo1",
bar: "bar1"
},
"id2": {
foo: "foo2",
bar: "bar2"
}
}
}
This store evolves by receiving full new values of items:
const reduceItems = function(items = {}, action) {
if (action.type === 'RECEIVE_ITEM') {
return {
...items,
[action.payload.id]: action.payload,
};
}
return items;
};
I want to display a Root view that renders a list of SubItem views, that only extract a part of the state.
For example the SubItem view only cares about the foos, and should get it:
function SubItem({ id, foo }) {
return <div key={id}>{foo}</div>
}
Since I only care about "subpart" of the states, that's what I want to pass to a "dumb" Root view:
const Root = function({ subitems }) {
// subitems[0] => { id: 'id1', foo: "foo1" }
// subitems[1] => { id; 'id2', foo : "foo2" }
const children = subitems.map(SubItem);
return <div>{children}</div>;
};
I can easily connect this component to subscribe to changes in the state:
function mapStatesToProps(state) {
return {
subitems: xxxSelectSubItems(state)
}
}
return connect(mapStatesToProps)(Root)
My fundamental problem is what happens when the part of the state that I don't care about (bar) changes.
Or even, when I receive a new value of an item, where neither foo nor bar has changed:
setInterval(() => {
store.dispatch({
type: 'RECEIVE_ITEM',
payload: {
id: 'id1',
foo: 'foo1',
bar: 'bar1',
},
});
}, 1000);
If I use the "naive" selector implementation:
// naive version
function toSubItem(id, item) {
const foo = item.foo;
return { id, foo };
}
function dumbSelectSubItems(state) {
const ids = Object.keys(state.items);
return ids.map(id => {
const item = state.items[id];
return toSubItem(id, item);
});
}
Then the list is a completely new object at every called, and my component gets rendered everytime, for nothing.
Of course, if I use a 'constant' selector, that always return the same list, since the connected component is pure, it is re-renderered (but that's just to illustrate connected components are pure):
// fully pure implementation
const SUBITEMS = [
{
id: 'id0',
foo: 'foo0',
},
];
function constSelectSubItems(state) {
return SUBITEMS;
}
Now this gets a bit tricky if I use an "almostConst" version where the List changes, but contains the same element.
const SUBITEM = {
id: 'id0',
foo: 'foo0',
};
function almostConstSelectSubItems(state) {
return [SUBITEM];
}
Now, predictably, since the list is different, even though the item inside is the same, the component gets rerendered every second.
This is where I though 'reselect' could help, but I'm wondering if I am not missing the point entirely. I can get reselect to behave using this:
const reselectSelectIds = (state, props) => Object.keys(state.items);
const reselectSelectItems = (state, props) => state.items;
const reselectSelectSubItems = createSelector([reSelectIds, reSelectItems], (ids, items) => {
return ids.map(id => toSubItem(id, items));
});
But then it behaves exactly like the naive version.
So:
is it pointless to try to memoize an array ?
can reselect handle this ?
should I change the organisation of the state ?
should I just implement shouldComponentUpdate on the Root, using a "deepEqual" test ?
should I give up on Root being a connected component, and make each LeafItems be connected components themselves ?
could immutable.js help ?
is it actually not an issue, because React is smart and will not repaint anything once the virtual-dom is computed ?
It's possible what I'm trying to do his meaningless, and hides an issue in my redux store, so feel free to state obvious errors.
You're definitely right about the new array references causing re-renders, and sort of on the right track with your selectors, but you do need to change your approach some.
Rather than having a selector that immediately returns Object.keys(state.item), you need to deal with the object itself:
const selectItems = state => state.items;
const selectSubItems = createSelector(
selectItems,
(items) => {
const ids = Object.keys(items);
return ids.map(id => toSubItem(id, items));
}
);
That way, the array will only get recalculated when the state.items object is replaced.
Beyond that, yes, you may also want to look at connecting your individual list item components so that each one looks up its own data by ID. See my blog post Practical Redux, Part 6: Connected Lists, Forms, and Performance for examples. I also have a bunch of related articles in the Redux Techniques#Selectors and Normalization and Performance#Redux Performance sections of my React/Redux links list.

Categories