I'm working on a project where we would like run multiple fetches in parallel to a very slow API. Ideally, we would like to populate our interface for the user as this data is received and do so in a summative manner. These requests may or may not resolve in the order that the API calls were made.
Most use cases of Promise.all with a setState involve setting state after all promises have resolved. However, what I'm looking to do demands setting state as a side effect within the child promises themselves, I believe.
So this is (simplified) what I am doing to achieve this:
const request = async (setState, endpoint) => {
const response = await fetch(endpoint);
const data = response.json();
setState(state => ({ ...state, ...data }))
}
// Called within React component as a side effect
const fetchAllData = (setState) => {
Promise.all(
[
request(setState, url_1),
request(setState, url_2),
request(setState, url_3)
]
)
}
Now, I'm running some testing and this does appear to work. I believe I should not be running into race conditions with the state because setState is being passed a function. However, I do wonder if I'm doing something dangerous with respect to React, updating state, and rendering.
Is there anything wrong with this picture?
There's nothing wrong with immediately updating state from each individual promise; this will work just fine. You might have a race condition if every request attempts to update the same bit of data, but as long as they write different parts of your state you should be fine (the state updater callback pattern is necessary though).
The only thing wrong with your code is the missing error handling, and that the Promise.all is currently a bit superfluous.
Related
How can I await, to resolve/reject, the promise returned by a function wrapped in the react hook "useMemo"?
Currently, my code looks like this:
// Get the persisted email/username
const persistedUsername = useMemo(async () => {
let username;
try {
username = await AsyncStorage.getData(`#${ASYNC_STORAGE_KEY}:username`);
} catch {}
return username;
}, []);
EDIT
What I am trying to achieve is to get the data before the component is rendered, some kind of "componentWillMount" life-cycle. My two options were:
Computing the value using useMemo hook to avoid unnecessary recomputations.
Combine useEffect + useState. Not the best idea because useEffect runs after the component paints.
#DennisVash has proposed the solution to this problem in the comments:
Blocking all the visual effects using the useLayoutEffect hook (some kind of componentDidMount/componentDidUpdate) where the code runs immediately after the DOM has been updated, but before the browser has had a chance to "paint" those changes.
As you can see, persistedUsername is still a promise (I am not waiting the result of the asynchronous function)...
Any ideas? Is it not a good idea to perform asynchronous jobs with this hook? Any custom hook?
Also, what are the disadvantages of performing this operation in this way compared to using useEffect and useState?
Same thing with useEffect and useState:
useEffect(() => {
// Get the persisted email/username
(async () => {
const persistedUsername = await AsyncStorage.getData(
`#${ASYNC_STORAGE_KEY}:username`
);
emailOrUsernameInput.current.setText(persistedUsername);
})();
}, []);
Thank you.
Seems like the question is about how to use componentWillMount with hooks which is almost equivalent to useLayoutEffect (since componentWillMount is deprecated).
For more additional info, you should NOT resolve async action in useMemo since you will block the thread (and JS is single-threaded).
Meaning, you will wait until the promise will be resolved, and then it will continue with computing the component.
On other hand, async hooks like useEffect is a better option since it won't block it.
I am confused right now. Learning js frontend and how to make API requests.
Till now, whenever I made an API call, I always used something, that let me handle an asynchronous request (axios or thunk).
But now I got a case, where I am making a request to my Firebase/Firestore and in the yt video, that I watched, only useEffect was used. Without any async/await, like:
useEffect(() => {
// snapshot - to check, if there is a change (like a new entry) in the database
db.collection('products').onSnapshot((snapshot) => {
setProducts(snapshot.docs.map((doc) => doc.data()));
});
}, []);
Why is this?
In the example code snippet
useEffect(() => {
// snapshot - to check, if there is a change (like a new entry) in the database
db.collection("products").onSnapshot(snapshot => {
setProducts(snapshot.docs.map(doc => doc.data()))
})
}, []);
db.collection("products") manages its own asynchronous event handling. When a "snapshot" event is received it invokes the onSnapshot callback and updates state. There is nothing to await on, especially since there is no value returned.
You should also note that useEffect hook callbacks are 100% synchronous code, they can't be marked async and await any code calls. They can, however, call async functions that do their own awaiting and such.
About the questions in your title
Use a useEffect hook any time you want to issue any side-effect from the component lifecycle, like console logging state, make data fetches, dispatching actions. This is mostly due to the fact that the entire function body of a functional react component is to be considered a pure function. useEffect with dependencies is more akin to a class-based component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods. Alone they are just called as part of the react component lifecycle.
Use axios, or fetch, or any other data fetching code whenever you need to fetch that data.
useEffect() - It is a hook that allows us to perform side effects in functional components like network request etc.
Axios - It is just a library where we can make http request like fetch API in javascript and also axios is promise based.
Here is a little code snippet:
async componentDidMount() {
...
this.state.postList.forEach(element => {
this.fetchItem(element);
});
}
async fetchItem(query) {
...
this.setState( previousState => {
const list = [...previousState.data, data];
return { data: list };
});
}
I'm curious to know if using setState in every iteration of a forEach loop is a bad idea or not. I'm suspecting that it impacts performance, but I'd like to know for sure because this seemed like the simplest solution to this problem.
Here's an alternative approach: Update your fetchItem to just return the item. In your componentDidMount use Promise.all to get all the items and then commit them to the state in a single operation.
async componentDidMount() {
const items = await Promise.all(this.state.postList.map(element => fetchItem(element)));
this.setState({data: items});
}
async fetchItem(query) {
const item = await getItem(query) // however you accomplish this
return item;
}
I'm curious to know if using setState in every iteration of a forEach loop is a bad idea or not.
If it would be directly inside of an iteration then definetly yes, as React than has to merge all the updates you make during the iteration, which will probably take much more time than if you would just set the state after the loop.
In your case however, you do start an asynchronous action in each iteration, and as all asynchronous tasks finish at different times, the updates aren't run all at once. The main benefit of your approach is that if these asynchronous tasks take some time (e.g. if you fetch a lot of data for each), then some information can already be shown, while some are still loading. If all those asynchronous calls only load a small amount of data, well then you should actually change your API to deliver all the data at once. So it really depends on your usecase wether thats good or bad.
componentDidMount() {
Promise.all([OfferCreationActions.resetOffer()]).then( () => {
OfferCreationActions.updateOfferCreation('viewOnly', true);
OfferCreationActions.updateOfferCreation('loadingOffer', true);
Promise.all([
OfferCreationActions.loadBarcodes(),
OfferCreationActions.loadBrandBarcodes(),
OfferCreationActions.loadBrands(),
OfferCreationActions.loadPayers(),
OfferCreationActions.loadSegments(),
OfferCreationActions.loadTactics(),
OfferCreationActions.loadStates(),
]).then(() => {
// let state settle before loading
setTimeout(() => {
OfferCreationActions.loadOffer(this.props.match.params.offerId);
}, 1500);
});
});
}
I'm working on a React app that needs to preload some data into state then load a larger object that references the preloaded data to map some fields. I've ran into a race condition in which some of the data from the promise chain is still being processed when I try to do the mapping. I added in the timeout yesterday but that doesn't feel like the best solution to me. I'm still fairly new to React and we are using Reflux as the store (if that makes a difference). Is there a better way to ensure all the data from the promise is currently being reflected in the state prior to making the call? Should I hook into componentShouldUpdate and check all of the fields individually?
There is a fundamental flaw with the way this is implemented! You are breaking the principle of uni directional data flow. Here are a few suggestions to fix it.
Do your side effect handling inside a seperate overarching function.
Handling promise race condition is a side effect(Something that is outside of React's UniFlow). So this is not a problem linked to "React". So as the first step onComponentDidMount delegate this race condition logic to a seperate action. Possibly do it inside "resetOfferOverall()" which is actually what is happening I guess.
Manage the promise inside the action and dispatch payloads to the store
In your code you are guaranteed that the "then" will be executed after the promise is resolved. Howerver the 2 calls to "updateOfferCreation" do not fall under this contract since it's outside the promise.all. May be they also need to come inside the massive promise.all section? Maybe they need to be completed before running the massive section. Just recheck this!
resetOfferOverall() {
Promise.all([OfferCreationActions.resetOffer()]).then( () => {
.then( () => {
// These are not guaranteed to be completed before the next "then" section!
OfferCreationActions.updateOfferCreation('viewOnly', true);
OfferCreationActions.updateOfferCreation('loadingOffer', true);
//*****************************************
Promise.all([
OfferCreationActions.loadBarcodes(),
OfferCreationActions.loadBrandBarcodes(),
OfferCreationActions.loadBrands(),
OfferCreationActions.loadPayers(),
OfferCreationActions.loadSegments(),
OfferCreationActions.loadTactics(),
OfferCreationActions.loadStates(),
]).then(() => {
OfferCreationActions.loadOffer(offerId);
});
});
}
If you want this sections to be completed before getting into that
massive promise all, change your code as follows.
async resetOfferOverall() {
Promise.all([OfferCreationActions.resetOffer()]).then( () => {
.then( () => {
await OfferCreationActions.updateOfferCreation('viewOnly', true);
await OfferCreationActions.updateOfferCreation('loadingOffer', true);
//await will stop code execution until the above async code is completed
Promise.all([
OfferCreationActions.loadBarcodes(),
OfferCreationActions.loadBrandBarcodes(),
OfferCreationActions.loadBrands(),
OfferCreationActions.loadPayers(),
OfferCreationActions.loadSegments(),
OfferCreationActions.loadTactics(),
OfferCreationActions.loadStates(),
]).then(() => {
//Now JS Guarantees that this call will not be called until everything above has been resolved!
OfferCreationActions.loadOffer(offerId);
});
});
}
Make sure that the actions you are waiting are returning a promise
Whatever pomises you wait on, if you do not actually return the relevant promise that is within the call itself, your code will not work properly.Let's consider the load barcodes action and Let's assume you use axios to fetch data.
loadBarcodes(){
// This "return" right here is vital to get your promises to behave properly
return axios.get('https://localhost:8080/api/barcodes/').then((response) =>{
//DISPATCH_TO_STORE
});
//If you did not return this promise this call will resolve immediately
}
On your component watch for the relevent Store. Show a loader until the payload is loaded to the store.
As you can see by relying on a store update to show the data we do not break the unidirectional data flow.
I am implementing a search input in my React app which may take several seconds for a response from the server based on the size of the result set. The issue I am having right now is if the user searches for a term which has a large result set and then searches for a term with a small result set straight after, the first search is returning after the last search and is overwriting the data, therefore displaying incorrect results on the table.
Currently I have a search component which calls an async function in its parent (this function can be called from several child components, Search being one of them). Inside the async function I have an await call to the service with the search query. Once that returns the results are passed to a function which updates some state.
I have read about cancel tokens but i'm not totally sure how to implement this. When the search component makes the initial call, there will be a promise which will be pending until the result is received. When I search again, how would I be able to ignore the result of the first promise? Each time I search, would I store the promise in a field of the component and somehow check this field in future searches?
I read many possible solutions to this online. I am using fetch-retry to handle the API call and would rather not use a library such as bluebird or axios. The main part I don't understand is how to have my promise not resolve if a promise has been created in future.
Hope I explained this well enough!
Thanks
When I search again, how would I be able to ignore the result of the first promise?
You probably don't want that, as youbare wasting some bandwith if you do a request and ignore its result. Instead you should cancel the underlying request (not the promise, promises can't be canceled directly).
To do so you could keep an abortion controller for each search:
this.controller = new AbortController();
Then launch all fetch requests as:
fetch(url, { signal: this.controller.signal })
.then(res => res.json())
.then(res => this.setState({ /*...*/ }))
.catch(() => /*..*/)
Now if you restart the search just do:
this.controller.abort();
this.controller = new AbortController();
and do the fetching again.
Read on