What's the best approach to using the results of one fetch request to make another fetch request to a different endpoint? How can I confirm the first fetch has completed and setState has happened?
class ProductAvailability extends React.Component {
state = {
store_ids: []
}
componentDidMount() {
fetch(`myapi.com/availability?productid=12345`)
.then((results) => {
return results.json();
})
.then((data) => {
const store_ids = data.result.map((store) => {
return store.store_id
})
this.setState({store_ids: store_ids})
})
/* At this point I would like to make another request
to myapi.com/storedata endpoint to obtain information
on each store in the state.store_ids, and add details
to the state */
}
render() {
return (
<div>
<p>STORE INFO FROM STATE GOES HERE</p>
</div>
)
}
}
When you do setState, it updates the component, so the natural point to read state after you've done a setstate, if you'd have read the docs, is in componentDidUpdate(prevProps, prevState).
I leave you to the doc: https://reactjs.org/docs/react-component.html#componentdidupdate
Attention: Don't use willupdate, it's unsafe as you read in the docs.
A further consideration could be done. If you could avoid to put these data in the state, you could also do everything in the componendDidMount (with promiseall for all the other requests maybe) and then set the state with the old and new data, this is preferable since you update your component only once.
Easier to do with async/await (.then/.catch requires a bit more work -- also, reference to Bluebird's Promise.each function). For clarity, its best to move this out of componentDidMount and into its own class method.
As a side note, if this action is happening for every product, then this secondary query should be handled on the backend. That way, you only need to make one AJAX request and retrieve everything you need in one response.
componentDidMount = () => this.fetchData();
fetchData = async () => {
try {
const productRes = fetch(`myapi.com/availability?productid=12345`) // get product data
const productData = await productRes.json(); // convert productRes to JSON
const storeIDs = productData.map(({store_id}) => (store_id)); // map over productData JSON for "store_ids" and store them into storeIDs
const storeData = [];
await Promise.each(storeIDs, async id => { // loop over "store_ids" inside storeIDs
try {
const storeRes = await fetch(`myapi.com/store?id=${id}`); // fetch data by "id"
const storeJSON = await storeRes.json(); // convert storeRes to JSON
storeData.push(storeJSON); // push storeJSON into the storeData array, then loop back to the top until all ids have been fetched
} catch(err) { console.error(err) }
});
this.setState({ productData, storeData }) // set both results to state
} catch (err) {
console.error(err);
}
}
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.
import {fetchData} from "../common/helpers";
import {updateData} from "../Data/ducks/actions"
fetchData will check for is data available in local storage or not otherwise it will make an api call to get the data.
class Data {
dataCheck(id) {
if (isDataAvailable) {
let data;
const fetchEligibleData = async () => {
let dataAvailabilityCheck = fetchData("DATA_EMPLOYEE");
data = dataAvailabilityCheck;
if (dataAvailabilityCheck instanceof Promise) {
data = await dataAvailabilityCheck;
}
return data;
}
const eligibleData = fetchEligibleData();
Store.dispatch(updateData({ availableData: eligibleData }))
}
}
}
export default new Data
in the above code the important part starts from if(isDataAvailable).
payload for the updateData action is always being sent as promise. this is not expected behaviour.
first promise should be resolved then the data will be passed to the updateData action.
Please suggest me the approach to solve the promise before store action being triggered.
Set variable data to the result of the promise if it is successful. I.E. fetch(APIURL).then(res => data = res);
Or, fetch(APIURL).then(async res => data = await res.JSON());
I realized your api call is elsewhere, and it looks like you're querying it for specific 'Employee' info. Without seeing that, I'm not sure what you're doing there. I don't know that you should be awaiting a promise at that point, but if you are, you still will need to get the result of it.
I have created a js file with API call that return array and I have filter specific filter
API return array:
const apiValues = ['value1', 'value2']
const checkIfExist = () => {
fetch('api/profile')
.then(response => response.json())
.then(result => (
Object.values(response.apiValues).filter(value => value === 'value2');
);
};
export default checkIfExist;
So, if API return 'value2' I need to show a component in another file:
import checkIfExist from './checkIfExist.js'
if(checkIfExist) {
return (
<SpecificChildComponent />
);
}
When I see the page, api is called but HTTP status is 404 Not Found
Your function is not returning a value. You either need to alter your function to return a promise or update it to accept a callback. Depending on your browser support/tooling you can use async/await to simplify the code.
async const checkIfExist = () => {
const response = await fetch('api/profile');
const result = response.json();
return Object.values(response.apiValues).filter(value => value === 'value2');
};
const exists = await checkIfExist();
Its not clear from your example but you should not put this code into the render path of your components. Render is called many times and placing this call in render will block the whole render process every time it rerenders. You should ideally perform this call either componentDidMount or depending on your code just somewhere outside the render path.
Working on a small project in React, and I'm using Axios to grab information from an API that is tested working. The problem is when I pull in the data and try to update the component state, the values coming in from the API aren't getting filled into the state to be rendered in the component.
I've been trying to get the data in the state like so:
componentDidMount() {
this.state.databases.forEach(d => {
axios.get(`http://localhost:3204/customer/${d}/count`).then(value => {
this.setState({ counts: this.state.counts.push(value.data) });
});
});
console.log(this.state);
}
a previous version was written like so:
componentDidMount() {
let internalCounts = [];
this.state.databases.forEach(d => {
axios.get(`http://localhost:3204/customer/${d}/count`).then(value => {
internalCounts.push(value.data);
});
});
this.setState({count: internalCounts});
}
Both situations end up with the same result. The state is never really updated with the new count array. I have the suspicion that this is due to the async nature of Axios and setState.
What would be a simple, effective way to get these two to stop racing each other?
In the first version you are correct that it is due to the async nature of the service. You are logging the output, but this is happening before the service has resolved.
In the second (previous) example you are setting the state before it resolves. I would recommend the following approach, creating an array of the promises then waiting for all of them to resolve before you update state:
componentDidMount() {
const promises = [];
this.state.databases.forEach(d => {
promises.push(axios.get(`http://localhost:3204/customer/${d}/count`));
});
Promise.all(promises).then(results => {
const internalCounts = [];
results.map(result => {
internalCounts.push(result.data);
});
this.setState({count: internalCounts});
})
}
Don't use console.log immediately after setting the state since it is asynchronous. Do it with a callback to setState.
this.setState({...}, () => console.log(...))
or inspect your state in your render method with console.log
Also, rather than doing it forEach, try to make all the request in one time with Promise.all if this suits you.
componentDidMount() {
const promiseArray = this.state.databases.map( d =>
axios.get(`http://localhost:3204/${d}/count`)
)
Promise.all( promiseArray )
.then( ( results ) => {
const data = results.map( result => result.data);
this.setState( prevState => ({
counts: [ ...prevState.counts, ...data]
}))
})
}
The issues was I was attempting to mutate state. (I've been horrible with immutable data.... so something I'll have to get used to.)
My solution code is as follows:
componentDidMount(){
this.state.databases.forEach(d => {
axios.get(`http://localhost:3204/${d}/count`).then(value => {
this.setState(prevState => ({
counts: [...prevState.counts, value.data]
}));
});
});
}
A few problems with your code:
Array.prototype.push(value.data) returns the new length, not the new array. So this.setState({ counts: this.state.counts.push(value.data) }); is making state.counts into a number value this.state.counts.length + 1, which is probably not desired.
If you want to print out the new state, the console.log(this.state); should be put into the callback param of setState().
Your previous version did not await for the axios call before setState. You got the idea right.