I am fetching an api and pushing results to an empty array, but I need the array to be filled to display the information within the array. I am getting "cannot read property high of undefined" i'm assuming because the array is not filled before rendering, how can I wait for the for loop to be complete before rendering the page?
function TopStocks(props) {
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
const stockInfo = [];
useEffect(() => {
fetchSymbols();
}, []);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(
`api&symbol=${symbols[i]}`
)
.then((res) => res.json())
.then((allStocks) => {
try {
let metaDataEntries = allStocks["Meta Data"];
let symbol = metaDataEntries["2. Symbol"].toUpperCase();
let pastDataEntries = allStocks["Time Series (Daily)"];
let pastDataValues = Object.values(pastDataEntries);
let mostRecentValue = pastDataValues[0];
let x = Object.values(mostRecentValue);
let open = parseFloat(x[0]).toFixed(2);
let high = parseFloat(x[1]).toFixed(2);
let low = parseFloat(x[2]).toFixed(2);
let close = parseFloat(x[3]).toFixed(2);
let percentage = close - open;
let result = parseFloat(percentage).toFixed(2);
stockInfo.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result,
});
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
}
return (<span className="header__grid-price">{stockInfo[0].high}</span>)
}
You are doing a simple push operation on stockInfo, which will not trigger the rerender, for that, you have to change the state of it, use useState hooks instead,
import React, { useEffect, useState } from "react";
function TopStocks(props) {
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
const [stockInfo, setStockInfo] = useState([]);
useEffect(() => {
fetchSymbols();
}, []);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(`api&symbol=${symbols[i]}`)
.then((res) => res.json())
.then((allStocks) => {
try {
let metaDataEntries = allStocks["Meta Data"];
let symbol = metaDataEntries["2. Symbol"].toUpperCase();
let pastDataEntries = allStocks["Time Series (Daily)"];
let pastDataValues = Object.values(pastDataEntries);
let mostRecentValue = pastDataValues[0];
let x = Object.values(mostRecentValue);
let open = parseFloat(x[0]).toFixed(2);
let high = parseFloat(x[1]).toFixed(2);
let low = parseFloat(x[2]).toFixed(2);
let close = parseFloat(x[3]).toFixed(2);
let percentage = close - open;
let result = parseFloat(percentage).toFixed(2);
let temp = [...stockInfo];
temp.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result
});
setStockInfo(temp);
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
}
return (
<span className="header__grid-price">
{stockInfo[0].high ? stockInfo[0].high : "loading"}
</span>
);
}
You are correct in your assumption. Try using the map function as it is async along with Promise.all so that you block the return of that function until all of your calls have finished.
...
Promise.all(
symbols.map(symbol => {
fetch(`api&symbol=${symbol}`)
.then(
// whatever you want to do with it
)
})
);
React hates it when you mutate constants. It's better to use the useState method to create the constant and the setter method.
const [stockInfo, setStockInfo] = useState([]); //This will default stockInfo to an empty array.
Then make your response dump into an array and setStockInfo() that array.
ex:
let newStockInfo = [];
/* Your stock gathering function here, but dumps into newStockInfo */
setStockInfo(newStockInfo);
That will update your stocks and prevent undefined properties.
I'm sure there is another way to do this, but this is a pretty quick way to get it done.
Related
I am querying each point of a polygon object (e in this case) using esri leaflet. To do so I have the following function that I use on a polgon that is passed to it:
const queryFeature = (e: any) => {
let nearbyCollection: any = new Array()
console.log(e)
const latlngParcel = new L.Polygon(e)
console.log(latlngParcel)
for (let n=0; n < e[0].length; n++) {
let point = L.latLng(e[0][n])
featureRef
.query()
.nearby(point, 1)
.run(function (error: any, featureCollection: any) {
if (error) {
console.log(error);
return;
} else {
featureCollection.features.forEach((i: any) => {
nearbyCollection.push({ id: i.id, name: i.properties.Situs1})})
}
})
};
return nearbyCollection
};
Which when I run console.log on the array "nearbyCollection" it shows it's contents however if I run nearbyCollection.length it is 0.
Furthermore I cant iterate over the array or pass this information to a new Array...
I am guessing it is because I am pushing to this array during an async call but I am not sure how to resolve this.
the problem is indeed the async call you're making within your loop.
i'd recommend checking out 'Serializing with promises and async/await' section of the answer below...
Asynchronous Process inside a javascript for loop
it sounds a little strange that you're making a web request to do a spatial query for each individual vertex of your parcel polygons, but i'm sure you have your reasons.
I agree with John's response. I think what he is suggesting you is to do something like this:
async const queryFeature = (e: any) => {
let nearbyCollection: any = new Array()
console.log(e)
const latlngParcel = new L.Polygon(e)
console.log(latlngParcel)
for (let n = 0; n < e[0].length; n++) {
let point = L.latLng(e[0][n])
await featureRef
.query()
.nearby(point, 1)
.run(function(error: any, featureCollection: any) {
if (error) {
console.log(error);
return;
} else {
featureCollection.features.forEach((i: any) => {
nearbyCollection.push({
id: i.id,
name: i.properties.Situs1
})
})
}
})
}
return nearbyCollection;
};
const nearbyCollection = queryFeature.then(nearbyCollection => nearbyCollection).catch(err => {
console.log(err);
});
Note: Update, I'm checking do not 100% sure if this will work, in case it doesn't, I would wrap the featureRef.query().nearby(point, 1)... in a function like this.
I have the following synchronism problem. Given that I know that the React useState hook is asynchronous, I run into the following: I'm downloading some images from Amazon S3, I manage to save it correctly in my hook: defaultSelfiePicture and depending on the weight of the image (or so I think) sometimes I get the images loaded correctly and sometimes not. I have tried to force state changes after I finish saving the object in my hook but it never renders the image, only if I change component and come back is when it is shown in the cases that it takes longer to load.
const [defaultSelfiePictures, setDefaultSelfiePictures] = useState([])
useEffect(() => {
if (savedUser.docs !== undefined) {
loadAllPictures()
}
}, [savedUser.docs.length])
const loadAllPictures = () => {
let p1 = loadUrlDefaultFrontPictures()
let p2 = loadUrlDefaultBackPictures()
let p3 = loadUrlDefaultSelfiePictures()
Promise.all([p1, p2, p3]).then(result => {
console.log('end all promises')
setTimestamp(Date.now())
})
}
const loadUrlDefaultSelfiePictures = async () => {
if (savedUser.docs.length > 0) {
let readedPictures = []
for (let i = 0; i < savedUser.docs.length; i++) {
if (
savedUser.docs[i].type === 'SELFIE'
//&& savedUser.docs[i].side === 'FRONT'
) {
if (
savedUser.docs[i].s3Href !== null &&
savedUser.docs[i].s3Href !== undefined
) {
const paramsKeyArray =
savedUser.docs[i].s3Href.split('')
let paramsKey = paramsKeyArray.pop()
let params = {
Bucket: process.env.REACT_APP_S3_BUCKET,
Key: paramsKey
}
await s3.getSignedUrl('getObject', params, function (err, url) {
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: url
})
})
} else {
let urlPicture = savedUser.docs[i].localHref
let response = await axios.get(`${URL_IMG}${urlPicture}`, {
responseType: 'blob'
})
function readAsDataURL(data) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(data)
reader.onloadend = () => {
resolve(reader.result)
}
})
}
const base64Data = await readAsDataURL(response.data)
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: `data:image/jpeg;base64,${base64Data.slice(21)}`
})
}
}
}
setDefaultSelfiePictures(readedPictures)
}
}
And I obtain this :
I can see that the hook has content, but that content is not updated until the next rendering of the component, also if I try to make any changes when I detect that the .length has changed it tells me that it is 0...
And right after the next render I get this:
I am trying to set state to my setStockInfo hook and wait for the for it to be set before I run props.onInitialSet(). I tried passing setStockInfo as a dependency in the useEffect, but getting no luck. I need the information from stockInfo before I set to the global store. Is there a way I can use a callback since useState doesn't return a promise? The error returning is "cannot read property symbol of undefined" from the props.initialSet()
function TopStocks(props) {
const [stockInfo, setStockInfo] = useState([]);
const symbols = ["AAPL", "NFLX", "GOOGL", "TSLA"];
let temp = [];
useEffect(() => {
fetchSymbols();
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}, [setStockInfo]);
async function fetchSymbols() {
for (let i = 0; i < symbols.length; i++) {
await fetch(
`api${symbols[i]}`
)
.then((res) => res.json())
.then((allStocks) => {
console.log(allStocks);
try {
let metaDataEntries = allStocks["Meta Data"];
let symbol = metaDataEntries["2. Symbol"].toUpperCase();
let pastDataEntries = allStocks["Time Series (Daily)"];
let pastDataValues = Object.values(pastDataEntries);
let mostRecentValue = pastDataValues[0];
let x = Object.values(mostRecentValue);
let open = parseFloat(x[0]).toFixed(2);
let high = parseFloat(x[1]).toFixed(2);
let low = parseFloat(x[2]).toFixed(2);
let close = parseFloat(x[3]).toFixed(2);
let colorToSend;
let percentage = close - open;
if (percentage < 0) {
colorToSend = "red";
} else {
colorToSend = "rgb(30, 216, 139)";
}
let result = parseFloat(percentage).toFixed(2);
temp.push({
symbol: symbol,
high: high,
low: low,
close: close,
open: open,
percentage: result,
color: colorToSend,
});
} catch {
console.log("surpassed the limit of 4 requests in under a minute");
}
});
}
setStockInfo(temp);
}
}
const mapDispatchToProps = (dispatch) => {
return {
onInitialSet: (symbol, percentage, price) => {
dispatch({
type: "INITIALSET",
value: {
price: price,
symbol: symbol,
percentage: percentage,
},
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TopStocks);
stockInfo is the value that will change, not the state setter. Inside the useEffect, check to see if the array has been populated:
// Run once, on mount:
useEffect(fetchSymbols, []);
// Run after fetchSymbols finishes:
useEffect(() => {
if (stockInfo.length) {
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}
}, [stockInfo]);
You should also put a try/catch around the await fetch so you can catch possible network errors.
Given that useEffect runs after every render, the fetched stockInfo is not reflected until after the current rendering process.
Thus, stockInfo is undefined when passed to onInitialSet.
To solve this, follow the Single Responsibility Principle by simply splitting to two effects:
First effect is to fetch symbols
Set initial set
const [stockInfo, setStockInfo] = useState([]);
// First effect: to fetch
useEffect(async () => {
let temp;
for (let i = 0; i < symbols.length; i++) {
await fetch(`api${symbols[i]}`);
// TODO: add rest of code
}
setStockInfo(temp);
}, []);
// 2nd effect: call onInitialSet
useEffect(() => {
if (stockInfo.length === 0) {
return;
}
props.onInitialSet(
stockInfo[2].symbol,
stockInfo[2].percentage,
stockInfo[2].close
);
}, [stockInfo]);
Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};
I have an action which will take quite some time to complete. It will be looping through an array with over 250k entries. With each entry it will be doing some logic. I want to inform the user of the actions progress, which would be:
let progress = (i / array.length) * 100
How can I add this to redux store or components state so that I can communicate the current progress in the UI?
I tried dispatching an action (as you can see below) which updates the stores progress with the current iterator but the reducer only executes when the for loop is created.
export const generateKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
await dispatch(startGeneration()); <-- This only runs when I use a setTimeout for the following action.
setTimeout(() => {
dispatch(buildKeyMap(mapFrom, mapTo));
}, 100);
};
};
export const buildKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
let keyMap = {};
for (let i = 0; i < mapFrom.length; i++) {
await dispatch(reportProgress(i)); // <-- This does not update in store until for loop is done.
let randomIndex = randomIntFromInterval(0, mapTo.length - 1);
let key = mapFrom[i].toString();
let value = mapTo[randomIndex].toString();
Object.assign(keyMap, { [key]: value });
mapTo.splice(randomIndex, 1);
}
await dispatch(stopGeneration());
return { type: actionTypes.DELIVERKEYMAP, payload: keyMap };
}
};