How to modify useState array one by one in mapped promise - javascript

I have
const [items, setItems] = useState<any[]>(itemsB.map(() => ({ loading: true })));
and
itemsB.map(async (itemB: any, index: number) => {
searchItems(itemB).then(result => {
const newItems = [...items]; // Ref A
newItems[index] = result;
setItems(newItems);
})
})
Because the inner function is an async fetch, items come back in an unpredictable order, and I want to add them to items as soon as they're ready, changing the placeholder loading object with the actual result. This almost does that, but the items referenced at Ref A does not update, so it cycles through changing each thing and then at the end only the last item to have been retrieved appears.
If I do a Promise.all().then(), it waits until all items are retrieved to execute the then part, so I'm wondering how to set items as they are resolved.

The calls made to setItems are batched to optimize performance. If the previous state relies on the current state, use the overloaded version of setItems which takes the previousState as the first argument.
searchItems(itemB).then(result => {
setItems(items => {
const newItems = [...items]; // Ref A
newItems[index] = result;
return newItems;
});
})

Maybe this would work:
itemsB.forEach(async (itemB: any) => {
searchItems(itemB).then(result => {
setItems(prevItems => [...prevItems, result]);
});
});
Note: this should not be in the render function. you should place that in componentDidMount/Update or useEffect hook.

Use can use the async library to help you to solve the problem as below:
import eachOf from 'async/eachOf';
// assuming items is an array of async items
async.eachOf(itemsB, async (itemB: any, index: number, callback: ReturnType<string | void>) => {
// Perform operation on itemB here.
console.log('Processing item ' + itemB);
try {
const result = await searchItems(itemB);
const newItems = [...items]; // Ref A
// Settting-up new items
newItems[index] = result;
setItems(newItems);
callback();
}catch(e) {
callback(e)
}
}, function(err) {
// If any of the item processing produced an error, err would equal that error
if( err ) {
// One of the iterations produced an error.
// All processing will now stop.
console.log('An item failed to process');
} else {
console.log('All items have been processed successfully');
}
});

Related

I have an array of different IDs and I want to fetch data from each IDs . Is it possible?

I have an array of mongoDB ids.
const pId =  ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
I used forEach to seperate the IDs like :
pId.forEach((item)=>{
console.log(item)
})
but I have a database(products) from where I want to fetch data from. So I tried
const [product, setProduct] = useState([{}]);
useEffect(() => {
pId?.forEach((item) => {
const getProduct = async () => {
try {
const res = await userRequest.get("/products/find/" + item)
setProduct(res.data)
} catch (err) {
console.log(err)
}
}
getProduct()
})
}, [pId])
I used useState[{}] because I want to collect the data in an array of objects.
I used useState[{}] because I want to collect the data in an array of objects.
Your code isn't collecting objects into an array. It's setting each result of the query as the single state item (overwriting previous ones).
If you want to get all of them as an array, build an array; one way to do that is map with the map callback providing a promise of each element, then use Promise.all to wait for all of those promises to settle:
// The usual advice is to use plurals for arrays, not singulars ("products", not "product")
const [products, setProducts] = useState([]); // Start with an empty array
useEffect(() => {
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item)))
.then((products) => setProducts(products))
.catch((error) => console.log(error));
}
}, [pId]);
Note that if pId changes while one or more userRequest.get calls are still outstanding, you'll get into a race condition. If userRequest.get provides a way to cancel in-flight calls (like fetch does), you'll want to use that to cancel the in-flight calls using a cleanup callback in the useEffect. For example, if userRequest.get accepted an AbortSignal instance (like the built-in fetch does), it would look like this:
const [products, setProducts] = useState([]);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item, { signal })))
.then((products) => setProducts(products))
.catch((error) => {
if (!signal.aborted) {
console.log(error);
}
});
}
return () => {
controller.abort();
};
}, [pId]);
Again, that's conceptual; userRequest.get may not accept an AbortSignal, or may accept it differently; the goal there is to show how to cancel a previous request using a useEffect cleanup callback.
You can map through the ids and create a promise for each, then use Promise.all() and at last set the products state:
import React, {
useState,
useEffect
} from 'react'
const Example = () => {
const [products, setProducts] = useState([]);
const ids = ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
useEffect(() => {
if(ids) Promise.all(ids.map(id => userRequest.get("/products/find/" + id).then(r => r.data))).then(results => setProducts(results))
}, [ids])
}
I renamed some of the variables for clarity to the future visitors. (pId to ids and product to products).
You're overwriting your product array with an individual result from each request. A simple solution would be to append to the array instead:
setProduct(product => [...product, res.data]); // take the old array and append the new item
As T.J. Crowder rightly suggested in the comments, you would keep appending to the initial product state when using the simple setter, so you need to use callback form which receives the current state as a parameter and returns the update.
I suggest you rename that particular state to products/setProducts to make clear it's an array.
Not directly related to the question, but bear in mind that firing a huge number of individual requests may cause performance degradation on the client and backend; there are plenty of options to deal with that, so I am not going into more detail here.
yes, it's possible. just change your code a bit:
const [product, setProduct] = useState([]); // empty array is enough for initialzing
useEffect(() => {
async function doSomethingAsync() {
if(!pId) return;
let newArray = [];
for(let item of pId) {
try {
const res = await userRequest.get("/products/find/" + item)
newArray.push(res.data); // push data to temporary array
} catch (err) {
console.log(err)
}
}
// set new state once
setProduct(newArray);
}
doSomethingAsync();
}, [pId])

useCallback with updated state object - React.js

I have a POST API call that I make on a button click. We have one large state object that gets sent as body for a POST call. This state object keeps getting updated based on different user interactions on the page.
function QuotePreview(props) {
const [quoteDetails, setQuoteDetails] = useState({});
const [loadingCreateQuote, setLoadingCreateQuote] = useState(false);
useEffect(() => {
if(apiResponse?.content?.quotePreview?.quoteDetails) {
setQuoteDetails(apiResponse?.content?.quotePreview?.quoteDetails);
}
}, [apiResponse]);
const onGridUpdate = (data) => {
let subTotal = data.reduce((subTotal, {extendedPrice}) => subTotal + extendedPrice, 0);
subTotal = Math.round((subTotal + Number.EPSILON) * 100) / 100
setQuoteDetails((previousQuoteDetails) => ({
...previousQuoteDetails,
subTotal: subTotal,
Currency: currencySymbol,
items: data,
}));
};
const createQuote = async () => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
const handleQuickQuote = useCallback(createQuote, [quoteDetails, loadingCreateQuote]);
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
// remove deal if present
if (quoteDetails.hasOwnProperty("deal")) {
delete quoteDetails.deal;
}
handleQuickQuote();
}
const generalInfoChange = (generalInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
tier: generalInformation.tier,
}
));
}
const endUserInfoChange = (endUserlInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
endUser: endUserlInformation,
}
));
}
return (
<div className="cmp-quote-preview">
{/* child components [handleQuickQuote will be passed down] */}
</div>
);
}
when the handleQuickQuoteWithoutDeals function gets called, I am deleting a key from the object. But I would like to immediately call the API with the updated object. I am deleting the deal key directly here, but if I do it in an immutable way, the following API call is not considering the updated object but the previous one.
The only way I found around this was to introduce a new state and update it on click and then make use of the useEffect hook to track this state to make the API call when it changes. With this approach, it works in a weird way where it keeps calling the API on initial load as well and other weird behavior.
Is there a cleaner way to do this?
It's not clear how any children would call the handleQuickQuote callback, but if you are needing to close over in callback scope a "copy" of the quoteDetails details then I suggest the following small refactor to allow this parent component to use the raw createQuote function while children receive a memoized callback with the current quoteDetails enclosed.
Consume quoteDetails as an argument:
const createQuote = async (quoteDetails) => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
Memoize an "anonymous" callback that passes in the quoteDetails value:
const handleQuickQuote = useCallback(
() => createQuote(quoteDetails),
[quoteDetails]
);
Create a shallow copy of quoteDetails, delete the property, and call createQuote:
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
const quoteDetailsCopy = { ...quoteDetails };
// remove deal if present
if (quoteDetailsCopy.hasOwnProperty("deal")) {
delete quoteDetailsCopy.deal;
}
createQuote(quoteDetailsCopy);
}

Batching React updates across microtasks?

I have code that looks something like:
// File1
async function fetchData() {
const data = await fetch(...);
setState({ data });
return data;
}
// File2
useEffect(() => {
(async () => {
const data = await fetchData();
setState({ data });
})();
});
This triggers 2 React commits in 1 task. This makes my app less than 60FPS. Ideally, I'd like to batch the 2 setStates. Currently, it looks like this:
Pink represents React commits (DOM operations). The browser doesn't have a chance to repaint until the second commit is done. I can give the browser a chance to repaint by adding await new Promise(succ => setTimeout(succ, 0)); between the setStates, but it'll be better if I could batch the commits.
It's also pretty much impossible to refactor this, since the useState exists in separate files.
I tried unstable_batchedUpdates but it doesn't work with async.
You can group fetchData, when fetchData is called with the same argument the cache is checked for a promise and that promise is returned instead of creating a new one (make a new fetch).
When the promise resolves then that cache entry is removed so when component mounts again it will fetch again. To change this behaviour you can pass a different cache object to the group funciton.
//group function (will always return promise)
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//cache that removes cache entry after resolve
const createCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
//remove cache key when resolved
resolved: (key) => cache.delete(key),
//to keep cache:
//resolved: () => 'NO_OP',
};
};
//fetch data function
const fetchData = (...args) => {
console.log('fetch data called with', args);
return new Promise((resolve) =>
setTimeout(() => resolve(args), 1000)
);
};
//grouped fetch data
const groupedFetchData = createGroup(createCache())(
fetchData
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
I think you should be able to so something along the lines of this, the aim being to cache the calls for a certain amount of time and then pass them all to unstable_batchedUpdates at once.
import { unstable_batchedUpdates } from 'reactDOM'
import raf from 'raf'
const cache = []
let rafId = null
function setBatchedState(setState, data) {
cache.push({ setState, data })
if(!rafId) {
rafId = raf(() => {
unstable_batchedUpdates(() => {
cache.forEach(({setState, data}) => setState(data))
})
rafId = null
cache = []
})
}
}
export default setBatchedState
This is using requestAnimationFrame to debounce the calls to unstable_batchedUpdates, you may prefer to use setTimeout depending on your use case.

React state in render is unavailable inside return

I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.

How to make Observable.flatMap to wait to resolve

Playing with RxJS and React, I'm having problem of how to wait for data in Observable.fromPromise generated within map on another Observable.
I have async helper:
const dataStreamGenerator = (url = CLIENTS_DATA_URL) => (
Rx.Observable.fromPromise(fetch(url))
.flatMap(response => Rx.Observable.fromPromise(response.json()))
.catch(err => Rx.Observable.of(new Error(err)))
);
Then I have actions.fetchClients$ which is Rx.Subject:
actions.fetchClients$.map((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
}).map(val => {
console.log('GOT DATA IN REDUCER: ', val);
const error = (val instanceof Error) ? val.message : undefined;
const data = error ? undefined : val;
actions.receivedClientsData$.next({ data, error, ts: Date.now() });
return (state) => state;
})
(Yes, trying to mimick Redux in RxJS).
Whan I test the dataStreamGenerator, it works ok (with ava) and delivers data:
test('dataStreamGenerator', t => {
const ds$ = dataStreamGenerator(CLIENTS_DATA_URL);
return ds$.do((data) => {
t.is(data.length, 10);
});
});
(AVA automatically subscribe to observable and consume it, so there is no need to subscribe).
But the actions.fetchClients$.map((url = CLI... second map (starting... console.log('GOT DATA IN REDUCER: ', val);... is still getting the Observable and not the data from dataStream$.
I tried all possible combinations of map and flatMap in fetchClients$ code but still no luck.
My test code is:
test.only('fetchClients$', t => {
const initialState = {};
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
reducer$.subscribe(val => {
console.log('TEST SUBSCRIBE VAL: ', val);
t.pass();
});
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
});
I cant figure out how to wait to the Observable dataStreamGenerator(url); to emit data and not the Observable.
Thanks.
You need to flatten the results of what you returned from dataStreamGenerator.
actions.fetchClients$
//This is now a flatMap
.flatMap((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
});
The map operator delivers the value as-is down stream, whereas flatMap will flatten Observables, Arrays and Promises such that their values are what get propagated.
It works in the test case because you are directly subscribing to the Observable returned by dataStreamGenerator.

Categories

Resources