React hooks, update after multiple promises execution for bulk processes - reactjs

I am working on a component that reads a CSV file and sends a request per every line on the file. The process works great but I am trying to show feedback as the lines get successfully posted. The problem is that using the useState hook, the set function gets passed the moment on calling the function and not after each promise has been resolved. So I cannot append to the successful results array, the array keeps getting replaced with the last successful call.
The API calls are debounced by one second in order to prevent an overload to the server.
import React, {useState} from "react";
import CSVReader from "react-csv-reader";
import {post} from "../api";
function App() {
const [inserts, setInserts] = useState([])
const callApi = async (x) => {
let item = {
date: x.date,
value: x.value,
};
await post(`add-items`, item);
setInserts([...inserts, item])
};
const debouncedApiCall = (body, delay) => {
return new Promise((resolve) => {
const handler = () => callApi(body).then((x) => resolve(x));
setTimeout(handler, delay);
});
};
const insert = async (rows) => {
let timer = 0;
await Promise.all(
rows.map(async (x) => {
timer++;
return await debouncedApiCall(x, timer * 1000);
})
);
};
let onFileLoaded = (data) => {
insert(data).then((x) => console.log(x));
};
return (
<div>
<CSVReader onFileLoaded={onFileLoaded}/>
{JSON.stringify(inserts)}
</div>
);
}
export default App;

When your call API function is called, within its closure it captures the state of inserts. This means, that inserts is not always up to date. What you end up with is called a "stale closure".
To get around this, the mutation function provided by the useState method can accept a callback function. This callback function can recieve the latest state when calling the function. This is helpful in async operations.
your callApi function will become
const callApi = async (x) => {
let item = {
date: x.date,
value: x.value,
};
await post(`add-items`, item);
setInserts(prevState => [...prevState , item]) //prevState gets the latest state of inserts when setInserts is called
return (x); //will return this value once this async function finishes. similar to resolve(x)
};
I cant exactly debug your code, but I would think there is an unnecessary step. you should be able to change your insert function to await all your callApi's, and just return x from your callApi function (as I added above).
const insert = async (rows) => {
let timer = 0;
await Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
);
};
As a side note, Promise.all returns a promise with the actual array results of all your promises results. You can fetch them by adding a .then to the Promise.All and remove the async from the insert function, or await the result.
Async Based: insert returns a promise, so you will need to handle that in the calling function.
const insert = async (rows) => {
let timer = 0;
const results = await Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
);
return results; //array of all your x values for each row
};
None-async based: end of line, insert is the calling function
const insert = (rows) => {
let timer = 0;
Promise.all(
rows.map((x) => {
return callApi(x); //Promise.All wants an array of promises. async functions return a promise
})
).then((result) => {
//result is an array of all x values according to rows
});
};

Related

Firestore Async function

So I'm trying to use React Query with Firestore and usually this works pretty fine but recently I have been unable to make a function that returns a value.
const fetchProduct = async () => {
const querySnapshot = await getDocs(collection(db, "notes"));
const arr = [];
querySnapshot.forEach((doc) => {
setNotes(doc.data())
}).catch((error) => {
console.log(error);
return null;
});
return notes
}
I'm later trying to use this function with React Query like this -
const { isLoading, isError, data, error } = useQuery(['todos'], fetchProduct)
However the value of the {data} always return to undefined but however once the fetchProduct() function is called manually it all works.
Is there anything I'm missing or doing wrong?
Setting the state is an asynchronous operation in React (see docs), so the value of notes isn't set yet by the time your return notes runs.
If you want to return the value synchronously:
return querySnapshot.docs.map((doc) => doc.data());
More typically though, you'll want to put the code that depends on that return value into its own useEffect hook that depends on the notes state.
Also see:
The useState set method is not reflecting a change immediately
Why does calling react setState method not mutate the state immediately?
Is useState synchronous?
It should be like this, you should not set inside the foreach function
const fetchProduct = async () => {
const querySnapshot = await getDocs(collection(db, "notes"));
const notes = [];
querySnapshot.forEach((note) => {
notes.push({ ...note.data(), id: note.id })
}).catch((error) => {
console.log(error);
return null;
});
return notes;
}
// in the place of the calling the function
const notes = await fetchProduct();
setNotes(notes);
Note: don't use variable name as doc in foreach, instead use some other variable name like since doc is built in function of firebase you might have imported it

Why my useEffect that tries to get blockchain data is looping infinitely and my async func still returns Promise pending

I am trying to use async await inside a useEffect hook getting some data from a testnet blockchain but I am getting 2 problems:
The async function returns a Promise, why is that? Shouldn't async await automatically resolve the promise and give me the data? I tried to solve it with Promise.resolve but not working, it still tells me campaigns is a Promise in pending state.
It enters in an infinite loop and I still do not get why.
Here is the code:
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
return campaigns
}
const campaigns = getCampaigns();
setCampaigns(Promise.resolve(campaigns));
console.log('campaigns: ', campaigns);
})
You have no dependencies array.
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
return campaigns
}
const campaigns = getCampaigns();
setCampaigns(Promise.resolve(campaigns));
console.log('campaigns: ', campaigns);
}, [])
Try this
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
setCampaigns(campaigns);
}
getCampaigns();
}, []);
The empty array in useEffect call makes it behave like component did mount and only executes once (assuming factory methods are initialized on mount) and since the getDeployedCompanigns Promise is already resolved I'm simply setting the state in the getCampaigns function.
Read this article for details: https://devtrium.com/posts/async-functions-useeffect

calling an api request multiple times sequentially using redux-saga

I need to call an API request (with different request payloads) multiple times in my reactjs application. But, I don't want to call these multiple requests at the same time and I want to wait till one is completed and then another is called so that they are called sequentially. How can I do this using redux-saga?
Here is how I've impletemented my saga for this specific api, but I don't know how to modify it to work in the way I explained above:
export function* fetchChartData(action) {
const call = Effect.call;
const put = Effect.put;
try {
const response = yield call(chartApi.GetCharts, action.payload)
yield put({
type: ActionChartDataType.GET_CHART_DATA_SUCCESS,
payload: response.data.data
})
} catch (e) {
}
}
export function* getAllCharts() {
yield takeEvery(ActionChartDataType.GET_CHART_DATA_START, fetchChartData)
}
In fact, I need to have the action of GET_CHART_DATA_SUCCESS and then call the other GET_CHART_DATA_START. I really appreciate any help.
Here is an example of how to group a promise returning function in the way you describe:
const group = (promiseFunction) => {
let promise = Promise.resolve();
return (...args) => {
promise = promise
.catch(() => 'ignore')
.then(() => promiseFunction(...args));
return promise;
};
};
//a helper that returns a promise that resolves in one second
const later = (value) =>
new Promise((r) => setTimeout(() => r(value), 1000));
//promise returning function that returns passed argument a second later
const promiseFn = (value) => {
console.log('promiseFn called with', value);
return later(value).then((r) =>
r === 3 ? Promise.reject(r) : r
);
};
//grouped primiseFn
const groupedPromiseFn = group(promiseFn);
//test calling the grouped promise function
[1, 2, 3, 4, 5].forEach(
(value) =>
console.log('calling grouped with', value) ||
groupedPromiseFn(value).then(
(r) => console.log('resolved with:', r),
(r) => console.log('rejected with:', r)
)
);
You can use group to have chartApi.GetCharts behave in the way you want:
const groupedGetCharts = group(chartApi.GetCharts)
//later in your code:
const response = yield call(groupedGetCharts, action.payload)

Trouble fetching data with Async/Await

Hi I'm trying to fetch a country's data after which I want to fetch the names for its neighboring countries.
import React from 'react';
export default function DetailsRoute({ match }) {
const [countryData, setCountryData] = React.useState({});
const [borderCountries, setBorderCountries] = React.useState([]);
React.useEffect(() => {
fetchCountryData();
}, [])
const fetchCountryData = async () => {
/* fetch country by Name */
const response = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.country}`);
const fetchedData = (await response.json())[0];
setCountryData(fetchedData);
const neighbors = [];
/* Extract 'alphaCode' for each bordered country and fetch its real name */
fetchedData.borders.forEach(async (alphaCode) => {
const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
const fetchedNeighbor = await response.json();
neighbors.push(fetchedNeighbor.name);
});
/* THIS DOESN'T WAIT FOR NEIGHBORS TO BE FILLED UP */
setBorderCountries(neighbors);
}
return (
<article>
<h1>{countryData.name}</h1>
{borderCountries.map(countryName => <h2>{countryName}</h2>)}
</article>
)
}
As you can see, the setBorderCountries(neighbors) doesn't run asynchronously. But I have no idea how to make it wait for the forEach() loop to finish.
Somewhere on stackoverflow, I saw Promise.all() and tried to implement it but I really don't know if it's syntactically correct or not-
Promise.all(
fetchedData.borders.forEach(async (alphaCode) => {
const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
const fetchedNeighbor = await response.json();
neighbors.push(fetchedNeighbor.name);
})
)
.then(() =>
setBorderCountries(neighbors)
)
My question is how do I make the setBorderCountries(neighbors) wait until forEach() loop finishes filling up neighbors?
And maybe some suggested optimization to my code?
The forEach loop finishes immediately, since the waiting happens in the callbacks (only), but those callbacks are all called synchronously, and so the forEach finishes before any of the awaited promises resolve.
Instead use a plain for loop:
for (let alphaCode of fetchedData.borders) {
Now the await inside that loop is part of the top level async function, and so it works as you intended.
You can also consider using Promise.all if it is acceptable that promises are created without waiting for the previous one to resolve. And then you can await the result of Promise.all. In your attempt at this, you did not pass anything to Promise.all, as forEach always returns undefined. The correct way would use .map as follows:
const neighbors = await Promise.all(
fetchedData.borders.map(async (alphaCode) => {
const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`);
const fetchedNeighbor = await response.json();
return fetchedNeighbor.name; // return it!!
});
)
setBorderCountries(neighbors);
Note that here also the .map iteration finishes synchronously, but it returns an array of promises, which is exactly what Promise.all needs. The awaiting happens with the await that precedes Promise.all.
Promise.all takes an array of promises. Your code is passing the result of the forEach call (undefined) to Promise.all, which isn't going to do what you want.
If you use map instead you can create an array of request promises. Something along the lines of:
const fetchNeighbor = async (alphaCode) => {
return fetch(`https://restcountries.eu/rest/v2/alpha/${alphaCode}`)
.then(response => response.json())
.then(n => n.name);
}
const neighborNames = await Promise.all(fetchedData.borders.map(fetchNeighbor));
setBorderCountries(neighbors);
Array.map produces a new array by running the given function on every element in the source array. So these two are roughly equivalent:
const promises = fetchedData.borders.map(fetchNeighbor);
const promises = [];
fetchedData.borders.forEach(alphaCode => {
promises.push(fetchNeighbor(alphaCode));
});
You now have an array of promises (because that's what fetchNeighbor returns) you can pass to Promise.all:
const results = await Promise.all(promises);
Promise.all resolves with an array of the resolved promise values. Since fetchNeighbor ultimately resolves with the name, you now have an array of names.
const results = await Promise.all(promises);
console.log(results);
// ['Country A', 'Country B', 'Country C', ... ]
I think you should use something like this.
const start = async () => {
await asyncForEach([1, 2, 3], async (num) => {
await waitFor(50);
console.log(num);
});
console.log('Done');
}
start();
this article I think is a good resource to learn: async/await with for each

Get data from a promise. Or is it promise at all?

I store some data in IndexedDB and i use npm package localforage for it.
const retrieveData = async () => {
const keys = await localforage.keys()
const data = await keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
})
return data
}
Whenever I execute this function, I get a resolved Promise object, which values I cannot extract
async componentDidMount() {
let asyncData = retrieveData()
console.log(asyncData) // Promise object
asyncData = retrieveData().then(values => values)
console.log(asyncData) // Promise object anyways
}
How exactly should I get data from this Promise object?
const retrieveData = async () => {
const keys = await localforage.keys()
// The return value of "keys.map" is an array of promises since
// async automatically returns a Promise behind the scenes.
// Await works on a single promise, not an array of promises,
// so "data" will not contain the actual data.
const data = await keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
})
return data
}
Do:
const retrieveData = async () => {
const keys = await localforage.keys()
const data = await Promise.all(keys.map(async (key) => {
const item = await localforage.getItem(key)
return [item.username, item.compamy, item.email, item.lastUpdate]
}));
return data
}
Or use Bluebird's map which works out of the box in this scenario:
// The "then" function does not do anything. It returns values,
// but only does so to the next "then" function. There are no
// further then-functions so the return value is unused.
// "values" is merely a local variable so you won't be able to
// access it anywhere outside the fat arrow function.
// You could move the console log into "then".
asyncData = retrieveData().then(values => values)
// asyncdata is still the unresolved promise object, the "then"
// function has not been run yet (then on the line above will be run when
// all of the awaits in retrieveData have been successfully resolved.
console.log(asyncData)
Do:
async componentDidMount() {
const data = await retrieveData();
console.log(data);
}
Or:
componentDidMount() {
retrieveData().then(values => {
console.log(values);
});
}
you can use for of loop here, and it will be simpler, we dont use await keyword directly in a map iterator, instead we can use promise.all as mentioned in above answer.
const retrieveData = async () => {
const keys = await localforage.keys();
let data;
for (let key of keys) {
const item = await localforage.getItem(key);
data.push([item.username, item.compamy, item.email, item.lastUpdate]);
}
return data;
}
Try using the "await" reserved keyword before your retrieveData() method at componentDidMount(), since it's a promise, an async event, you have to wait until it finishes all of it's inner executions to return some data and go on.
Just like you did at retrieveData() declaration use await before the promise. in detail what you need:
async componentDidMount() {
let asyncData = await retrieveData()
....
}

Resources