I'm new about reactJs.
I'm trying to concatenate results in async loop, but something is wrong.
setState not save correctly, when I print it I can see it's an empty array, I think because there is inside an async call.
How I can solve that? Please suggest me.
function App() {
const [data, setData] = useState({data:[]});
const handleChildSubmit = (data) => {
setData(data);
}
return (
<div>
<Form onChildSubmit={handleChildSubmit} />
<Results flights={data} />
</div>
);
}
const Form = ( {onChildSubmit} ) => {
const [dati, setDati] = useState([]);
const onFormSubmit = async (e) => {
e.preventDefault();
// Flights search Parameters
const data = new FormData(e.target);
const dateFrom = data.get('dateFrom');
const dateTo = data.get('dateTo');
const departureDay = data.get('day');
const placeFrom = data.get('placeFrom');
const placeTo = data.get('placeTo');
let dayDeparture;
const daysDeparture = getDayOfWeekArray(dateFrom,dateTo,departureDay);
// Loop of Fly Search in range time
for(let i=0; i < daysDeparture.length; i++){
dayDeparture = daysDeparture[i];
axios.get('https://api.skypicker.com/flights?flyFrom='+placeFrom+'&to='+placeTo+'&dateFrom='+dayDeparture+'&dateTo='+dayDeparture+'&partner=picky')
.then(res => {
setDati([...dati, res.data]);
onChildSubmit(dati);
})
.catch(function (error) {
console.log('error: '+error);
})
.finally(function () {
});
}
}
The problem is that useState here gives you the dati variable with a specific value. Then your asynchronous stuff happens, and setDati() is called multiple times, but dati is does not change until the form is re-rendered and you then call onFormSubmit again.
You have a few options.
You could add the results only once as an array.
const results = []
for(let i=0; i < daysDeparture.length; i++){
dayDeparture = daysDeparture[i];
const res = await axios.get('https://api.skypicker.com/flights?flyFrom='+placeFrom+'&to='+placeTo+'&dateFrom='+dayDeparture+'&dateTo='+dayDeparture+'&partner=picky');
results.push(res.data);
}
// Add all results when all are fetched.
setDati([...dati, ...results])
Or useReducer which reliably gives you the latest version of the state, right at the time that you change it, so you don't have stale data.
// Something like...
function reducer(state, action) {
switch(action.type) {
case 'append':
return [...state, action.payload]
}
}
function YourComponent() {
const [dati, dispatch] = useReducer(reducer, [])
const onFormSubmit = async (e) => {
for(let i=0; i < daysDeparture.length; i++){
dayDeparture = daysDeparture[i];
const res = await axios.get('https://api.skypicker.com/flights?flyFrom='+placeFrom+'&to='+placeTo+'&dateFrom='+dayDeparture+'&dateTo='+dayDeparture+'&partner=picky')
dispatch({type: 'append', payload: res.data})
}
}
}
useState provides a way to use the previous state when updating (just like the callback way when you are using a class component), could give that a shot
.then(res => {
setDati(prevDati => ([...prevDati, res.data]));
onChildSubmit(dati);
})
Related
Question:
I am developing a small app that is a memory game of Formula One Drivers to practice React. It makes a call to an API to get the driver info then I have to make a second API call to Wikipedia to get the driver images. When I submit the year and click the button it will only load half the information Image 1 & getDrivers function. When I click the button again it will load the images Image 2 & getDriversImgs function / retrievingImgUrl.
I believe I am encountering a GOTCHA or doing something fundamentally wrong. I am not sure in my setDrivers call in the retrievingImgUrl() function if it isn't updating because it is a reference to an array even though I use map and it should be returning a new array?
Or is this something where I need to use useEffect or useCallback to have the code rerender in one go?
Any advice on how to fix the bug and if you could point me in a direction to possibly clean up these fetch calls or would you consider this clean code (like conceptually chaining fetch calls together in smaller functions or should I make it one big function)?
import { Fragment, useState, useEffect } from "react";
// Components
import Header from "./components/header/Header";
import CardList from "./components/main/CardList";
import Modal from "./components/UI/Modal";
// CSS
import classes from "./App.module.css";
function App() {
const [drivers, setDrivers] = useState([]);
const getDrivers = async (year) => {
const response = await fetch(
"https://ergast.com/api/f1/" + year + "/drivers.json"
);
const data = await response.json();
let driverInfo = [];
data.MRData.DriverTable.Drivers.map((driver) => {
driverInfo.push({
id: driver.code,
firstName: driver.givenName,
lastName: driver.familyName,
wikipedia: driver.url,
image: null,
});
});
setDrivers(driverInfo);
getDriversImgs();
};
async function getDriversImgs() {
console.log(drivers);
const responses = await Promise.all(
drivers.map((driver) => {
let wikiPageName = driver.wikipedia.split("/").slice(-1).toString();
let wiki_url =
"https://en.wikipedia.org/w/api.php?origin=*&action=query&titles=" +
wikiPageName +
"&prop=pageimages&format=json&pithumbsize=500";
return fetch(wiki_url);
})
);
const urls = await Promise.all(responses.map((r) => r.json())).then(
(json) => retrievingImgUrl(json)
);
setDrivers((prev) => {
return prev.map((item, idx) => {
return { ...item, image: urls[idx] };
});
});
}
const retrievingImgUrl = async (data) => {
console.log(data);
const strippingData = data.map((d) => {
return d.query.pages;
});
const urls = strippingData.map((d) => {
const k = Object.keys(d)[0];
try {
return d[k].thumbnail.source;
} catch {
return null;
}
});
return urls;
};
return (
<Fragment>
<Header getDrivers={getDrivers} />
<CardList drivers={drivers} />
</Fragment>
);
}
export default App;
Image 1 (clicked button once):
Image 2 (clicked button twice):
Object20Object error:
const Header = (props) => {
const driverYear = useRef();
const driverYearHandler = (e) => {
e.preventDefault();
console.log(driverYear);
const year = driverYear.current.value;
console.log(typeof year);
props.getDrivers(year.toString());
};
return (
<header className={classes.header}>
<Title />
<form onSubmit={driverYearHandler}>
{/* <label htmlFor="year">Enter Year:</label> */}
<input
type="text"
id="year"
ref={driverYear}
placeholder="Enter Year:"
/>
<button onClick={props.getDrivers}>Get Drivers</button>
</form>
</header>
);
};
export default Header;
Console Error:
UPDATED FETCH CALL
const getDrivers = async (year) => {
console.log("Running more than once??");
const url = "https://ergast.com/api/f1/" + year + "/drivers.json";
const response = await fetch(url);
const data = await response.json();
let driverInfo = [];
data.MRData.DriverTable.Drivers.map((driver) => {
driverInfo.push({
id: driver.code,
firstName: driver.givenName,
lastName: driver.familyName,
wikipedia: driver.url,
image: null,
});
});
getDriversImgs(driverInfo).then((data) => setDrivers(data));
console.log("Here is driver info", driverInfo);
};
const getDriversImgs = async (driverInfo) => {
const responses = await Promise.all(
driverInfo.map((driver) => {
let wikiPageName = driver.wikipedia.split("/").slice(-1).toString();
let wiki_url =
"https://en.wikipedia.org/w/api.php?origin=*&action=query&titles=" +
wikiPageName +
"&prop=pageimages&format=json&pithumbsize=500";
return fetch(wiki_url);
})
);
const urls = await Promise.all(responses.map((r) => r.json())).then(
(json) => retrievingImgUrl(json)
);
return driverInfo.map((item, idx) => {
return { ...item, image: urls[idx] };
});
};
const retrievingImgUrl = async (data) => {
const strippingData = data.map((d) => {
return d.query.pages;
});
const urls = strippingData.map((d) => {
const k = Object.keys(d)[0];
try {
return d[k].thumbnail.source;
} catch {
return null;
}
});
return urls;
};
This is likely happening because of a small misunderstanding with setState. You are calling getDriversImgs() just after setDrivers() is called, but any set state function is asynchronous. It is likely not done setting before you look for the driver's image.
The simplest solution in my opinion will be to not setDrivers until you've correlated an image to each driver. You already have all of your driverInfo in an array, so iterating through that array and finding the image for the driver should be quite straightforward.
After you've created a driverInfo array that includes the driver's image, then you can use setDrivers which will render it to the DOM.
i'm quite new to react-native, i'm trying to implementing a setting screen in my recipe search app. basically the user can choose different filter to avoid some kind of food (like vegan or no-milk ecc.), i thought to make an array with a number for each filter and then in the search page passing the array and apply the filter adding piece of strings for each filter. the thing is: useEffect render the array i'm passing with async-storage empty on the first render, it fulfill only on the second render, how can i take the filled array instead of the empty one?
const [richiesta, setRichiesta] = React.useState('');
const [data, setData] = React.useState([]);
const [ricerca, setRicerca] = React.useState("");
const [preferenza, setPreferenza] = React.useState([]);
let searchString = `https://api.edamam.com/search?q=${ricerca}&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getMyValue();
getPreferences(preferenza);
},[])
const getMyValue = async () => {
try{
const x = await AsyncStorage.getItem('preferenza')
setPreferenza(JSON.parse(x));
} catch(err){console.log(err)}
}
const getPreferences = (preferenza) => {
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
//useEffect
useEffect(() => {
getRecipes();
}, [ricerca])
//fetching data
const getRecipes = async () => {
const response = await fetch(searchString);
const data = await response.json();
setData(data.hits);
}
//funzione ricerca (solo scrittura)
const onChangeSearch = query => setRichiesta(query);
//funzione modifica stato di ricerca
const getSearch = () => {
setRicerca(richiesta);
}
//barra ricerca e mapping data
return(
<SafeAreaView style={styles.container}>
<Searchbar
placeholder="Cerca"
onChangeText={onChangeSearch}
value={richiesta}
onIconPress={getSearch}
/>
this is the code, it returns "error" because on the first render the array is empty, but on the second render it fills with the value 1. can anyone help me out please?
By listening to the state of the preferenza. You need to exclude the getPreferences(preferenza); out of the useEffect for the first render and put it in it's own useEffect like this:
...
useEffect(() => {
getMyValue();
}, [])
useEffect(() => {
if( !preferenza.length ) return;
getPreferences(preferenza);
}, [preferenza])
i forgot to put the index of the array in
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
thanks for the answer tho, have a nice day!
Last edit of the night. Tried to clean some things up to make it easier to read. also to clarify what is going on around the useEffect. Because I am running react in strict mode everything gets rendered twice. The reference around the useEffect makes sure it only gets rendered 1 time.
Db is a firebase reference object. I am grabbing a list of league of legends games from my database.
one I have all my games in the snapshot variable, I loop through them to process each game.
each game contains a list of 10 players. using a puuId I can find a specific player. We then pull the data we care about in addChamp.
The data is then put into a local map. We continue to update our local map untill we are done looping through our database data.
After this I attempt to change our state variable in the fetchMatches function.
My issue now is that I am stuck in an infinite loop. I think this is because I am triggering another render after the state gets changed.
import { useState, useEffect, /*useCallback,*/ useRef } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var init = new Map()
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
init.set(item.championName, temp)
//setChamps(new Map(champs.set(item.championName, temp)))
}//,[champs])
const pack = /*useCallback( /*async*/ (data) => {
console.log("enter pack")
for(const item of data.participants){
//console.log(champ.assists)
if(item.puuid === player.puuid){
console.log(item.summonerName)
if(init.has(item.championName)){//only checking init??
console.log("update champ")
}
else{
console.log("add champ")
/*await*/ addChamp(item)
}
}
}
}/*,[addChamp, champs, player.puuid])*/
const fetchMatches = async () => {
console.log("enter fetch matches")
Db.collection("summoner").doc(player.name).collection("matches").where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
//console.log("loop")
console.log(doc.id, " => ", doc.data());
console.log("total: ", ++total);
await pack(doc.data());
});
})
.then( () => {
setChamps(init)
})
.catch((error) => {
console.log("error getting doc", error);
});
}
const render1 = useRef(true)
useEffect( () => {
console.log("enter use effect")
if(render1.current){
render1.current = false
}
else{
fetchMatches();
}
})
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
Newest Version. no longer infinitly loops, but values do not display/render.
import { useState, useEffect } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
return temp
}
useEffect(() => {
var tempChamp = new Map()
Db.collection("summoner").doc(player.name).collection("matches").where("queueId","==",420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
console.log(doc.id," => ", doc.data());
console.log("total: ", ++total);
for(const person of doc.data().participants){
if(player.puuid === person.puuid){
console.log(person.summonerName);
if(tempChamp.has(person.championName)){
console.log("update ", person.championName);
//add update
}else{
console.log("add ", person.championName);
var data = await addChamp(person);
tempChamp.set(person.championName, data);
}
}
}
})//for each
setChamps(tempChamp)
})
},[player.name, total, player.puuid]);
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
useEffect will be called only once when you will not pass any argument to it and useEffect works as constructor hence its not possible to be called multiple times
useEffect( () => {
},[])
If you pass anything as argument it will be called whenever that argument change is triggered and only in that case useEffect will be called multiple times.
useEffect( () => {
},[arg])
Though whenever you update any state value in that case component will re-render. In order to handle that situation you can use useCallback or useMemo.
Also for map operation directly doing it on state variable is not good idea instead something like following[source]:
const [state, setState] = React.useState(new Map())
const add = (key, value) => {
setState(prev => new Map([...prev, [key, value]]))
}
I have made some edits to your latest code try following:
import { useState, useEffect, useRef } from "react";
import Db from "../Firebase";
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map());
const addChamp = (item) => {
let min = item.timePlayed / 60;
let kda = (item.kills + item.assists) / item.deaths;
let dub = null;
if (item.win) {
dub = 1;
} else {
dub = 0;
}
let temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled / min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1,
};
setChamps((prev) => new Map([...prev, [item.championName, temp]]));
};
const pack = (data) => {
for (const item of data.participants) {
if (item.puuid === player.puuid) {
if (!champs.has(item.championName)) {
addChamp(item);
}
}
}
};
const fetchMatches = async () => {
Db.collection("summoner")
.doc(player.name)
.collection("matches")
.where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
await pack(doc.data());
});
})
.catch((error) => {});
};
const render1 = useRef(true);
useEffect(() => {
fetchMatches();
});
return (
<div>
<ul>
{[...champs.keys()].map((k) => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
);
};
export default TotGenStats;
i can't move my code to child component so how can i solve this problem. so that i can use my api data to my combobox
async getData() {
const PROXY_URL = 'https://cors-anywhere.herokuapp.com/';
const URL = 'my-api';
const res = await axios({
method: 'post', // i get data from post response
url: PROXY_URL+URL,
data: {
id_user : this.props.user.id_user
}
})
const {data} = await res;
this.setState({ user : data.data})
}
componentDidMount = () => {
this.getData()
}
and i send my state to my combobox in child component
<ComboBox
name="pic"
label="Default PIC"
placeholder=""
refs={register({ required: true })}
error={errors.PIC}
message=""
labelFontWeight="400"
datas={this.state.user}
></ComboBox>
combobox code :
right now I just want to be able to console my index data
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, []);
export default ComboBox;
I think you are missing the props.datas dependency in your ComboBox component.
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, [props.datas]); // THIS IS THE DEPENDENCY ARRAY, try adding props.datas here
export default ComboBox;
Here is a brief explanation of useEffect.
Used as componentDidMount():
useEffect(() => {}, [])
Used as componentDidUpdate() (triggers after props.something changes):
useEffect(() => {}, [props.something])
Used as componenWillUnmount():
useEffect(() => {
return () => { //Unmount }
}, [])
This, of course, is a really simple explanation, and this can be used much better when properly learned. Take a look at some tutorials utilizing useState, try to find in particular migrations from this.state to useState - those might help you wrap your head around useState
I am making dummy app to test server side API.
First request returns nested JSON object with Product names and number of variants that it has. From there I extract Product name so I can send second request to fetch list of variants with product images, sizes etc.
Sometimes it will load and display variants from only one product but most of the times it will work correctly and load all variants from both dummy products.
Is there a better way of doing this to ensure it works consistently good. Also I would like to know if there is a better overall approach to write something like this.
Here is the code:
import React, { useEffect, useState } from "react";
import axios from "axios";
import ShirtList from "../components/ShirtList";
const recipeId = "15f09b5f-7a5c-458e-9c41-f09d6485940e";
const HomePage = props => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
)
.then(response => {
let shirtList = [];
const itemsLength = response.data.Products.length;
response.data.Products.forEach((element, index) => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${element.ProductName}`
)
.then(response => {
shirtList.push(response.data.Variants);
if (index === itemsLength - 1) {
setLoaded(shirtList);
}
});
});
});
}, []);
const ListItems = props => {
if (props.loaded) {
return loaded.map(item => <ShirtList items={item} />);
} else {
return null;
}
};
return (
<div>
<ListItems loaded={loaded} />
</div>
);
};
export default HomePage;
You are setting the loaded shirts after each iteration so you will only get the last resolved promise data, instead fetch all the data and then update the state.
Also, separate your state, one for the loading state and one for the data.
Option 1 using async/await
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = async () => {
const { data } = await axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
return data.Products
}
const fetchShirts = async productName => {
const { data } = await axios.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
return data.Variants
}
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
const fetchProductShirts = async () => {
const products = await fetchProducts()
const shirts = await Promise.all(
products.map(({ productName }) => fetchShirts(productName)),
)
setShirtList(shirts)
setIsLoading(false)
}
fetchProductShirts().catch(console.log)
}, [])
}
Option 2 using raw promises
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = () =>
axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
.then(({ data }) => data.Products)
const fetchShirts = productName =>
axios
.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
.then(({ data }) => data.Variants)
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
fetchProducts
.then(products) =>
Promise.all(products.map(({ productName }) => fetchShirts(productName))),
)
.then(setShirtList)
.catch(console.log)
.finally(() => setIsLoading(false)
}, [])
}
Now you have isLoading state for the loading state and shirtList for the data, you can render based on that like this
return (
<div>
{isLoading ? (
<span>loading...</span>
) : (
// always set a unique key when rendering a list.
// also rethink the prop names
shirtList.map(shirt => <ShirtList key={shirt.id} items={shirt} />)
)}
</div>
)
Refferences
Promise.all
Promise.prototype.finally
React key prop
The following should pass a flat array of all variants (for all products ) into setLoaded. I think this is what you want.
Once all the products have been retrieved, we map them to an array of promises for fetching the variants.
We use Promise.allSettled to wait for all the variants to be retrieved, and then we flatten the result into a single array.
useEffect(()=>(async()=>{
const ps = await getProducts(recipeId)
const variants = takeSuccessful(
await Promise.allSettled(
ps.map(({ProductName})=>getVariants({ recipeId, ProductName }))))
setLoaded(variants.flat())
})())
...and you will need utility functions something like these:
const takeSuccessful = (settledResponses)=>settledResponses.map(({status, value})=>status === 'fulfilled' && value)
const productURL = (recipeId)=>`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
const variantsURL = ({recipeId, productName})=>`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`
const getProducts = async(recipeId)=>
(await axios.get(productURL(recipeId)))?.data?.Products
const getVariants = async({recipeId, productName})=>
(await axios.get(variantsURL({recipeId,productName})))?.data?.Variants