My Home.vue needs two async actions:
created() {
this.$store.dispatch('GET_LATEST_POLL');
this.$store.dispatch('INIT_STREAM');
},
This is their Vuex implementation:
GET_LATEST_POLL: async (context) => {
const pollData = await axios.get(`${BFF_ENDPOINT}/polls/last`, getAuthHeader(context));
const item = pollData.data.data;
context.commit('SET_LATEST_POLL', item);
},
INIT_STREAM: async (context) => {
const streamData = await axios.get(`${API_ENDPOINT}/polls/?obd=date`, getAuthHeader(context));
const items = streamData.data.data;
items.filter(item => item._id !== context.state.latestPoll._id);
context.commit('SET_STREAM', items);
},
I realized there is a dependency of INIT_STREAM to LATEST_POLL on context.state.latestPoll. I do not want to serialize both actions, I want they both talk to the backend paralelly. But then I need INIT_STREAM to wait for the LATEST_POLL result.
How can I achieve it? Do I have to merge the logic into the single action which fires two promises and then await Promise.all([latestPoll, items])? Is this correct approach?
I would suggest you to have a single action dispatched from your component which in turn dispatches other 2 actions which do not mutate anything.
LatestPollAndStreamAction: async (context) => {
let pollDataRequest = axios.get(...);
let streamDataRequest = axios.get(...);
Promise.all(pollDataRequest , streamDataRequest).then(([latestPoll, items]) => {
const pollItem = latestPoll.data.data;
context.commit('SET_LATEST_POLL', pollItem);
//logic for the other commit
const streamItems = streamData.data.data;
streamItems.filter(item => item._id !== context.state.latestPoll._id);
context.commit('SET_STREAM', streamItems);
})
}
This will ensure that both of your requests are fired in parallel and once you have the result from both using do the commits in desired order.
PS: The code is not tested but is just you give a gist of the desired approach
As I interpreted the question, you were suggesting creating a third action that merged the two actions. That approach sounds okay, but I think you could also adjust INIT_STREAM to make the calls in parallel. You need to:
Make sure GET_LATEST_POLL returns its promise.
Dispatch the action to GET_LATEST_POLL from INIT_STREAM, but don't await the result - just store the return value as const getLatestPollPromise.
Make the axios call for const streamData = await axios.get... but again, change it from await to const streamDataPromise = axios.get...
Use await Promise.all([getLatestPollPromise, streamDataPromise]) like you proposed in the question.
If INIT_STREAM always needs latestPoll, I see no need for a third action as that qualifies as a leaky abstraction and will probably confuse other developers.
Related
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])
I'm trying to make the home page send one API call on load and display all results on screen. It seems to send the call and receive response fine, although despite receiving the response from the server it can't pass the contents of the payload within the code, which is a JSON.
useEffect(() => {
const localUser = localStorage.getItem("user");
if (localUser) {
const foundUser = localUser;
setUser(foundUser);
} else {
const newUser = uuidv1();
localStorage.setItem(newUser, user);
setUser(newUser);
}
console.log(user);
async function fetchPosts() {
try {
let tempPosts = [];
const response = await fetch('http://localhost:3000/posts')
.then(response => response.json())
.then(response.payload.forEach(object => tempPosts.push(object.post)))
.then(setPosts((posts) => [tempPosts]));
console.log(posts);
} catch (err) {
console.log(err)
}
}
fetchPosts();
}, [user, posts]);
Somehow React is trying to access the response without the declaration and I have no idea how, which in result stops the function from executing.
Take a look at this line:
const response = await fetch('http://localhost:3000/posts')
.then(response => response.json())
You're combining two paradigms - asynchronous programming using Promises and callbacks with then, and asynchronous programming using async/await. You'll usually want to pick one or the other for use in a single function, and you definitely cannot combine them in a single line (or at least, not like this).
If you want to use async (and I would recommend this approach), you'll probably want something like this:
async function fetchPosts() {
let tempPosts = [];
const response = await fetch('http://localhost:3000/posts');
const data = await response.json();
data.payload.forEach(object => tempPosts.push(object.post))
return tempPosts;
}
I don't know what data looks like, so you may have to play with the 4th line of the function, but hopefully you get the gist. You'll probably want to define this function outside of your component, and certainly not within the useEffect hook.
An example of how you could use this to fetch posts on the first render of your component is
useEffect(() => {
fetchPosts().then(data => setPosts(data));
}, []);
assuming you have a relevant useState hook.
I have a React app built with the Minimal template and I'm trying to follow along with one of their tutorials, in order to create a Redux slice that feeds some data to a custom component. The data itself is collected from Firebase. Below is my code:
firebase.js - helper
export function getDocuments(col) {
const colRef = collection(db, col);
const q = query(colRef, where('uid', '==', auth.currentUser.uid));
getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
// return [1,2,3]
}
product.js - Redux slice
export function getProducts() {
return async (dispatch) => {
dispatch(slice.actions.startLoading());
try {
const products = await getDocuments('products');
dispatch(slice.actions.getProductsSuccess(products));
} catch (error) {
dispatch(slice.actions.hasError(error));
}
};
}
ProductList.js - component
const dispatch = useDispatch();
const { products } = useSelector((state) => state.client);
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
useEffect(() => {
if (products.length) {
// setTableData(products);
}
}, [products]);
If I console log data in the helper function (firebase.js), I get the values I expect, once the promise is resolved/fulfilled. However, if I console.log clients in the product.js slice or later in the component, I get undefined.
I assume my problem is not being able to understand how async + await + useEffect work together in order to fix this. My assumption is that I am trying to access the value before the promise is resolved and therefore before the helper function returns it. I confirmed that by returning a simple array [1, 2, 3] in my helper function as a test.
I think I am missing something fundamental here (I am not very experienced with React and JS in general and still learning things on the go). Can someone help me understand what am I doing wrong?
Thank you!
With await you can await the fulfillment or rejection of a promise, but your getDocuments Function does not return a promise. Change the last line of the function to the following:
return getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
Async and Await are no different in React than in plain JavaScript:
When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method
useEffect():
By using this Hook, you tell React that your component needs to do something after rendering. This function will run every time the component is re-rendered.
After an input change in my input element, I run an empty string check(if (debouncedSearchInput === "")) to determine whether I fetch one api or the other.
The main problem is the correct promise returned faster than the other one, resulting incorrect data on render.
//In my react useEffect hook
useEffect(() => {
//when input empty case
if (debouncedSearchInput === "") autoFetch();
//search
else searchvalueFetch();
}, [debouncedSearchInput]);
searchvalueFetch() returned slower than autoFetch() when I emptied the input. I get the delayed searchvalueFetch() data instead of the correct autoFetch() data.
What are the ways to tackle this? How do I queue returns from a promises?
I read Reactjs and redux - How to prevent excessive api calls from a live-search component? but
1) The promise parts are confusing for me
2) I think I don't have to use a class
3) I would like to learn more async/await
Edit: added searchvalueFetch, autoFetch, fetcharticles code
const autoFetch = () => {
const url = A_URL
fetchArticles(url);
};
const searchNYT = () => {
const url = A_DIFFERENT_URL_ACCORDING_TO_INPUT
fetchArticles(url);
};
const fetchArticles = async url => {
try{
const response = await fetch(url);
const data = await response.json();
//set my state
}catch(e){...}
}
This is an idea how it could looks like. You can use promises to reach this. First autoFetch will be called and then searchvalueFetch:
useEffect(() => {
const fetchData = async () => {
await autoFetch();
await searchvalueFetch();
};
fetchData();
}, []);
You can also use a function in any lifecycle depends on your project.
lifecycle(){
const fetchData = async () => {
try{
await autoFetch();
await searchvalueFetch();
} catch(e){
console.log(e)
}
};
fetchData();
}
}
I have read a lot about async await, but apparently I still don't get it. ;-)
I am trying to transform the following .then promise structure into async await:
componentDidMount() {
const { store } = this.props
Promise.all([
API.fetchTodos(),
API.fetchGoals(),
]).then(([ todos, goals ]) => {
store.dispatch(receiveDataAction(todos, goals))
})
store.subscribe(() => this.forceUpdate())
console.log('test')
}
My result is:
async componentDidMount() {
const { store } = this.props
const [todos, goals] = await Promise.all([
API.fetchTodos(),
API.fetchGoals(),
])
store.dispatch(receiveDataAction(todos, goals))
store.subscribe(() => this.forceUpdate())
console.log('test')
}
The result is that this function never ends. It calls everything including the console.log, but then the program just stops (no error). I'm not showing you any other parts of the application because according to my understanding these two functions should be equivalent - so the rest should not matter. Apparently I am wrong! :-) What am I doing wrong and why doesn't my solution work?
The difference between your two snippets is that in the second async/await example, you don't subscribe to the store until after you've fetched the goals and todos, whereas in the first, you subscribe immediately.
So your second example isn't working since now you've guaranteed that
store.dispatch(receiveDataAction(todos, goals))
is called before
store.subscribe(() => this.forceUpdate())
and since the action has already been dispatched by that point, the subscription callback is never called.
To fix that, you might just want to move the subscription part so that it occurs before the await call. That way you are already subscribed before the promise has resolved. So something like this:
async componentDidMount() {
const { store } = this.props
// Move to the front so this happens before the await.
store.subscribe(() => this.forceUpdate())
const [todos, goals] = await Promise.all([
API.fetchTodos(),
API.fetchGoals(),
])
store.dispatch(receiveDataAction(todos, goals))
console.log('test')
}