Set state once all data is pushed to array - arrays

I am trying to make a 'facebook kind of newsfeed' in React with Firebase Firestore. In the componentDidMount I first get the friendslist and per friend I will get their activities which I push to an empty array and sort() + reverse() the id's which are timestamps. This way the newest activity will be first in the array. Once ALL the items are pushed to the array, I want to set the state with the array. This is the code that I have:
import React, { Component } from 'react'
import { db, firebaseAuth } from '../../helpers/base'
import Activities from './Activities'
export default class ActivityList extends Component {
state = {
activityKeys: [],
}
componentDidMount(){
const uid = firebaseAuth().currentUser.uid
var activityKeys = []
db.doc(`users/${uid}/social/friends`).get().then( (doc) => {
//GET ALL THE FRIENDS
const friends = doc.data() //OBJECT OF {friendOneId: "friendOneId", friendTwoId: "friendTwoId"}
//LOOP THROUGH FRIENDS AND GET ACTIVITY
Object.keys(friends).forEach( friend => {
db.collection("activity").where("user", "==", friend).get().then((querySnapshot) => {
querySnapshot.forEach( (doc) => {
const activity = doc.id
activityKeys.push(activity)
activityKeys.sort().reverse()
})
})
})
})
console.log('activityKeys: ', activityKeys)
this.setState({ activityKeys })
}
render () {
return (
<div>
<h5>Activity List</h5>
<Activities activityKeys={this.state.activityKeys} />
</div>
)
}
}
The problem is that the array isn't set correctly or that it maybe is set before all the items are pushed. This is the log that I get:
It looks like it is loaded but it is empty between the brackets. If I console.log This.state.activity I get the same result. Can someone tell me how to fix this? And how can I setState once all the activities are pushed to the empty array?

You get a Promise when you make a call to API, but you set this.setState({ activityKeys }) before the call has completed. In other words, you must chain another .then() after the data has been received, in which you will call this.setState({ activityKeys }). What makes it a little difficult is that you're creating many Promises when iterating with forEach, and you need to wait for each of them to complete. You could save them all to list, and use Promise.all to wait for their completion and return it from the previous .then. Read more on the promises: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Related

setState is not updating in my other method

I'm very new to react and i'm confused why my state is not updated in another method of mine see example below.
fetchMovies = () => {
const self = this;
axios.get("https://api.themoviedb.org/3/trending/movie/day?api_key=XXXXXXX")
.then(function(response){
console.log(response.data)
self.setState({
collection: response.data.results
})
console.log(self.state.collection)
});
}
makeRow = () => {
console.log(this.state.collection.length);
if(this.state.collection.length !== 0) {
var movieRows = [];
this.state.collection.forEach(function (i) {
movieRows.push(<p>{i.id}</p>);
});
this.setState({
movieRow: movieRows
})
}
}
componentDidMount() {
this.fetchMovies();
this.makeRow();
}
When inside of fetchMovies function i can access collection and it has all the data but this is the part i can't understand in the makeRow function when i console log the state i would of expected the updated state to show here but it doesn't i'm even executing the functions in sequence.
Thanks in advance.
the collection is set after the async call is resolved. Even though makeRow method is called after fetchMoview, coz of async call, u will never know when the call will be resolved and collection state will be set.
There is no need to keep movieRows in the state as that is just needed for rendering. Keeping html mockup in the state is never a good idea.
So u should just call fetchMoviews in the componentDidMount and render the data in as follows:
render() {
const { collection } = this.state;
return (
<>
{
collection.map(c => <p>{c.id}</p>)
}
</>
)
}
make sure the initial value for collection in the state is [] .
The setState() documentation contains the following paragraph:
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
To access the modified state you need to use the function signature setState(updater, [callback]), so in your case it should be;
self.setState({
collection: response.data.results
}, () => { // Will be executed after state update
console.log(self.state.collection)
// Call your make row function here and remove it from componentDidMount if that is all it does.
self.makeRow()
} )

How to make a limited axios request?

could someone lend me a hand at this?
I have a code that is working fine, it shows a list of all fire type Pokemon, the names and the images. It is showing all fire pokemon (100 pokemon more or less), but i want to request ONLY the first 10, the is a way to do this? (i have tried to put "params: _limit:10" at the axios.get function but didnt work.
export default class PokemonList extends Component {
state= {
url: 'https://pokeapi.co/api/v2/type/fire',
pokedata: null
};
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokedata: res.data['pokemon'] })
}
render() {
return (
<PanelArea>
{this.state.pokedata ? (
<div className='row'>
{this.state.pokedata.map(pokedata =>
<PokemonCard
key={pokedata.pokemon.name}
name={pokedata.pokemon.name}
url={pokedata.pokemon.url}
/>
)}
</div>
) : (
<h1> Carregando Pokemon </h1>
)}
</PanelArea>
)};
}
What you are trying to achieve is strictly an API level change.
Also pokemon API has a limit param that could be used to set the limit of items requested. I might suggest try to use that.
If for some reason that isn't working, you can have a workaround in your client-side code where you fetch the pokemon data.
Let's say that you are getting 100 items returned in an array. Before setting local state with this value, you could filter over the incoming results and make sure that you are just accepting the first 10 or last 10 or whatever your implementation maybe.
Update with further details:
The response which comes in, is an object, which has a key named, pokemon, this is an array. All you need to do is before setting the local state with pokemon data, have a wrapper function do certain manipulations.
function mapPokemonData = (data) => {
// let's assume you always need only ten items and that the API is
// always going to return you data more than 10
let updatedData = [];
if(data.length >= 10) {
updatedData = [...data].splice(0, 10)
} else {
updatedData = [...data]
}
return updatedData
}
Now you can take the returned value from this function and then set the local state value.
I have discovered a way that worked for me, i just added .slice(0,10) after the res.data request and worked out fine!
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokedata: res.data['pokemon'].slice(1,10) })
}

Accessing JSON Array Data in React.js/Next.js Combo

Given an API which returns JSON data like:
["posts":
{"id":1,
"name":"example",
"date":"exampledate",
"content":"examplecontent",
"author":"exampleauthor"},
{"id":2,
..]
The length of the array is unknown.
I am fetching data via isomorphic-fetch like this:
displayPosts.getInitialProps = async function() {
const res = await fetch('.../post');
const data = await res.json();
return{
posts: data.posts
}
}
which is working (console.log.stringify(data)).
Now i want to display such posts on my displayPosts page.
Therefore i am using the following React Component.
class Posts extends React.Component {
stat = {
// here i don't know how to set the state
}
render() {
return (
// i want to render the data here
);
}
}
export default Posts;
Question: How do i set a state, so that i can neatly display every post in my displayPosts.js page with
<Posts posts={props.Posts}/>
?
class Posts extends React.Component {
state = {
posts: []
}
componentDidMount() {
this.savePosts();
}
componentDidUpdate() {
this.savePosts();
}
savePosts = () => {
if(this.props.posts){
//do any other processing here first if you like
this.setState({
posts: this.props.posts
});
}
}
You probably don't need to save the posts in state, since you could just pull them from props directly. But if you need to process or transform them somehow, it might make sense.
In either case, you just hook into the lifecycle methods to do this.
Note: This will set the state every time the component updates. Often you only want to update the state when that specific prop changes. If so, you can first compare the old and new props to see if it has changed in a way that means you want to update your state.

What is the best way to make multiple get request with axios in a loop?

I'm having a problem making multiple request in a loop.
I'm making a react app that renders multiple components called Cards. inside each card I want to make some requests so I got this.
componentWillMount(){
if(this.props.movies){
let promises = []
this.props.movies.forEach((item, i) => {
console.log(item)
let movieUrl = `http://localhost:3000/movies/${item}`
promises.push(axios.get(movieUrl))
})
axios.all(promises).then(res => console.log(res))
}
}
Movies is an array that I get from the father component.
so apparently is working because I get results but tit is always with the last element of the last card. Here is an image:
You should avoid using forEach when you really need to map and build the url with item.imdbID instead of item
componentWillMount(){
if(this.props.movies){
const promises = this.props.movies.map(item => {
const movieUrl = `http://localhost:3000/movies/${item.imdbID}`
console.log(movieUrl)
return axios.get(movieUrl)
)
Promise.all(promises).then(results => console.log(results))
}
}
Edit1: removed async/await due to incompatible build configuraton
Edit2: used item.imdbID instead of item and logged urls
You can use async/await. Look:
async componentWillMount(){
if(this.props.movies){
const results = []
this.props.movies.forEach((item, i) => {
const movieUrl = `http://localhost:3000/movies/${item}`
const result = await axios.get(movieUrl)
results.push(result)
})
// console.log(results)
}
}
Have you tried using bluebird's Promise.mapSeries?
import Promise from 'bluebird'
componentWillMount(){
if(this.props.movies){
Promise.resolve(this.props.movies)
.mapSeries(item => axios.get(`http://localhost:3000/movies/${item}`))
.then(movies => console.log(movies))
}
}

Passing the response of an API call as a parameter for another API call

I'm trying to make an API call that has for parameter a state set by another call :
I'll explain my code below !
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
coinList: [],
coinInfos: []
};
}
componentDidMount() {
// FIRST CALL HERE: I get a list of every coin
axios.get('https://min-api.cryptocompare.com/data/all/coinlist')
.then(res => {
const coins = res.data;
console.log(coins);
this.setState({ coinList: coins.Data });
});
// SECOND CALL HERE: I want to get the price of every coin of the previous list
if (this.state.coinList == null) {
return null;
}
else {
axios.get('https://min-api.cryptocompare.com/data/pricemultifull?fsyms=' + this.state.coinList + '&tsyms=USD')
.then(response => {
const coinCap = response.data;
this.setState({ coinInfos: coinCap.RAW });
});
}
render() {
return(
<div className="App">
{Object.keys(this.state.coinInfos).map((key) => (
<div className="container">
<span className="left">{key}</span>
<span className="right">{this.state.coinInfos[key].USD.MKTCAP}</span>
</div>
))}
</div>
);
}
}
I'm using an if condition for my second call because this.state.coinList returns 2 empty arrays and 1 array in which are the data (I don't know why there are 2 empty arrays by the way).
This code works for the first call, but not for the second.
I'm a beginner in React so I looked at the doc, I think the problem is that the 1st call doesn't render before the second call, so this.state.coinList is empty.
Can you tell me if I'm wrong ? And if I'm true, where should I make my second API call ?
I hope I'm clear, thank you for your time !
Here is the API's doc if you need : https://www.cryptocompare.com/api/#-api-data-
The calls are synchronous. You get to the second call before it even finishes the first one. As the simplest solution I would suggest to put the second call in the then callback function of your first call.
In there you will have the response data of the first call for sure and then you can work with it and pass it to the second call.
componentDidMount() {
// FIRST CALL HERE: I get a list of every coin
axios.get('https://min-api.cryptocompare.com/data/all/coinlist')
.then(res => {
const coins = res.data;
console.log(coins);
this.setState({ coinList: coins.Data });
// SECOND CALL HERE: I want to get the price of every coin of the previous list
if (this.state.coinList == null) {
return null;
}
else {
axios.get('https://min-api.cryptocompare.com/data/pricemultifull?fsyms=' + this.state.coinList + '&tsyms=USD')
.then(response => {
const coinCap = response.data;
this.setState({ coinInfos: coinCap.RAW });
});
});
}
You are making asynchronous calls for operations you want to perform synchronously. Then First answer is correct but I would prefer to use async/await
// Put async before the componentDidMount so you can use async/await
async componentDidMount() {
// FIRST CALL HERE: I get a list of every coin
const resp1 = await axios.get(
"https://min-api.cryptocompare.com/data/all/coinlist"
);
const coins = resp1.data;
/* Placing the condition above the set state ensures
unnecessary setting of state and rending of components would not happen */
if (!coins) return;
this.setState({ coinList: coins });
// SECOND CALL HERE: I want to get the price of every coin of the previous list
const resp2 = axios.get(
`https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coins}&tsyms=USD`
);
this.setState({ coinInfos: resp2.data.RAW });
}

Resources