How to fetch data from multiple urls at once? - javascript

I have a function that fetches from a url in React
const DataContextProvider = (props) => {
const [isLoading, setLoading] = useState(false);
const [cocktails, setCocktails] = useState([]);
useEffect(() => {
const fetchCocktailList = async () => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/';
setLoading(true);
try {
const res = await fetch(`${baseUrl}search.php?s=margarita`);
const data = await res.json();
console.log(data);
setCocktails(data.drinks);
setLoading(false);
} catch (err) {
console.log('Error fetching data');
setLoading(false);
}
};
fetchCocktailList();
}, []);
How I'm mapping data so far.
const DrinkList = () => {
const { cocktails } = useContext(DataContext);
return (
<div className='drink-list-wrapper'>
{cocktails.length > 0 &&
cocktails.map((drink) => {
return <DrinkItem drink={drink} key={drink.idDrink} />;
})}
</div>
);
};
However I want to fetch from this url also ${baseUrl}search.php?s=martini
I would like a good clean way to do this and set my state to both of the returned data.

First base the data fetch function on a parameter:
const fetchCocktail = async (name) => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/';
try {
const res = await fetch(`${baseUrl}search.php?s=` + name);
const data = await res.json();
return data.drinks;
} catch (err) {
console.log('Error fetching data');
}
}
Then use Promise.all to await all results:
setLoading(true);
var promises = [
fetchCocktail(`margarita`),
fetchCocktail(`martini`)
];
var results = await Promise.all(promises);
setLoading(false);
DrinkList(results);
Where results will be an array with the responses that you can use on the DrinkList function.

Here's a method which will let you specify the cocktail names as dependencies to the useEffect so you can store them in your state and fetch new drink lists if you want new recipes. If not, it'll just be a static state variable.
I've also added another state variable errorMessage which you use to pass an error message in the case of failure.
Also, you should include the appropriate dependencies in your useEffect hook. The setState functions returned by calls to useState are stable and won't trigger a re-run of the effect, and the cocktailNames variable won't trigger a re-run unless you update it with new things to fetch.
const DataContextProvider = (props) => {
const [isLoading, setLoading] = useState(false);
const [cocktails, setCocktails] = useState([]);
const [errorMessage, setErrorMessage] = useState(''); // holds an error message in case the network request dosn't succeed
const [cocktailNames, setCocktailNames] = useState(['margarita', 'martini']); // the search queries for the `s` parameter at your API endpoint
useEffect(() => {
const fetchCocktailLists = async (...cocktailNames) => {
const fetchCocktailList = async (cocktailName) => {
const baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
const url = new URL(baseUrl);
const params = new URLSearchParams({s: cocktailName});
url.search = params.toString(); // -> '?s=cocktailName'
const res = await fetch(url.href); // -> 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s=cocktailName'
const data = await res.json();
const {drinks: drinkList} = data; // destructured form of: const drinkList = data.drinks;
return drinkList;
};
setLoading(true);
try {
const promises = [];
for (const cocktailName of cocktailNames) {
promises.push(fetchCocktailList(cocktailName));
}
const drinkLists = await Promise.all(promises); // -> [[drink1, drink2], [drink3, drink4]]
const allDrinks = drinkLists.flat(1); // -> [drink1, drink2, drink3, drink4]
setCocktails(allDrinks);
}
catch (err) {
setErrorMessage(err.message /* or whatever custom message you want */);
}
setLoading(false);
};
fetchCocktailList(...cocktailNames);
}, [cocktailNames, setCocktails, setErrorMessage, setLoading]);
};

var promises = [
fetchCocktail(api1),
fetchCocktail(api2)
];
var results = await Promise.allSettled(promises);

Related

Cancel fetch with continue request in a custom hook

I have created a custom hook to fetch data. In that hook I have an async function looking something like this:
const useData = () => {
const [enabled, setEnabled] = useState(true);
const [data, setData] = useState(false);
const fetchData = async () => {
const response = await fetch();
const data = await response.json();
while (data.continue && enabled) {
response = await fetch(data.requestId);
data = await response.json();
}
};
useEffect(() => {
console.log(enabled);
}, [enabled]);
return { data, setEnabled };
};
When I call setEnabled(false) from a component it's set to false when the useEffect logs it but it continues to be true in the fetchData function and it never cancels the fetch which I was expecting.
This is because the fetchData callback function "captures" the state of the data variable when it is first called, and it doesn't know about the new value of data when you change it (this is known in React as stale closures). To fix this, you have to use the useRef hook instead:
const useData = () => {
const [enabled, setEnabled] = useState(true);
const [data, setData] = useState(false);
const isCancelled = useRef(false);
const fetchData = async () => {
const response = await fetch();
const data = await response.json();
while (data.continue && !isCancelled.current) {
response = await fetch(data.requestId);
data = await response.json();
}
};
useEffect(() => {
isCancelled.current = !enabled;
}, [enabled]);
return { data, setEnabled };
};

Upload in Firebase Storage and fetch link to add it in Realtime Database

I'm using Firebase Realtime Database and Firebase Storage in this application, the goal is to upload in Firebase Storage the images in the pictures array, and then get the link of the Firebase Storage to that image and add it in the object which will be pushed in imagesUriArray and added to Realtime Database. The problem is that when I press addItem it successfully update the id, but the images parameter remains empty. And in fact, imagesUriArray remains empty unless I refresh the screen.
export default function NewItem({ route, navigation }) {
const [pictures, setPictures] = useState([]);
const [imagesUriArray, setImageUriArray] = useState([]);
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
})
const addItem = async () => {
uploadImages()
const changes = ref(db, path)
get(changes).then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const fetchedArray = snapshot.val().data
let array = fetchedArray;
let object = {
"id": `${Math.random()}`,
"images": imagesUriArray,
}
array.push(object)
update(changes, {
data: array
})
}
})
}
}
const uploadImages = () => {
const metadata = {
contentType: 'image/jpeg',
};
pictures.forEach( async (obj) => {
const id = obj.id
const uri = obj.uri
const response = await fetch(uri);
const blob = await response.blob();
var ref = storageUpload(storage, path)
await uploadBytes(ref, blob, metadata)
await getDownloadURL(ref)
.then((metadata) => {
let array = imagesUriArray
let object = {
"id": id,
"uri": metadata
}
array.push(object)
setImageUriArray(array)
})
.catch((error) => {
console.log(error)
});
})
}
return(
..............
)
}
Issue
This appears to be a state mutation in the uploadImages callback. A reference to the imagesUriArray state is cached locally, mutated (i.e. direct push into array), and then the same reference is saved back into state. This doesn't trigger react to rerender with any updated state value.
const uploadImages = () => {
...
pictures.forEach( async (obj) => {
const id = obj.id
const uri = obj.uri
const response = await fetch(uri);
const blob = await response.blob();
var ref = storageUpload(storage, path)
await uploadBytes(ref, blob, metadata)
await getDownloadURL(ref)
.then((metadata) => {
let array = imagesUriArray // <-- reference to state
let object = {
"id": id,
"uri": metadata
}
array.push(object) // <-- state mutation
setImageUriArray(array) // <-- same state reference
})
.catch((error) => {
console.log(error)
});
})
};
Solution
Use a functional state update to update from the previous state. Create a shallow copy of the previous state and append the new data.
const uploadImages = () => {
...
pictures.forEach( async (obj) => {
const { id, uri } = obj;
const response = await fetch(uri);
const blob = await response.blob();
const ref = storageUpload(storage, path);
await uploadBytes(ref, blob, metadata);
await getDownloadURL(ref)
.then((uri) => {
setImageUriArray(imagesUriArray => [
... imagesUriArray, // <-- shallow copy
{ id, uri }, // <-- append new object
]);
})
.catch(console.log);
})
};
Update
uploadImages needs to return a Promise so that addItem can wait for it to complete its asynchronous code. addItem also needs to access the updated imagesUriArray that uploadImages updates.
Map the pictures array to an array of Promises (i.e. an async fetchImageUri function) that eventually returns the object with id and new uri properties.
const uploadImages = () => {
...
const fetchImageUri = async ({ id, uri }) => {
try {
const response = await fetch(uri);
const blob = await response.blob();
const ref = storageUpload(storage, path);
await uploadBytes(ref, blob, metadata);
const newUri = await getDownloadURL(ref);
return { id, uri: newUrl };
} catch(error) {
console.log(error);
}
}
return Promise.all(pictures.map(fetchImageUri));
};
Update addItem to wait for the resolved array of Promises that contain the uploaded image data. Enqueue the imagesUriArray state update here, then continue the rest of the function referencing the returned uploadedImages array from uploadImages.
const addItem = async () => {
const uploadedImages = await uploadImages();
setImageUriArray(imagesUriArray => [
...imagesUriArray, // <-- shallow copy
...uploadedImages, // <-- append new objects
]);
const changes = ref(db, path);
get(changes).then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const fetchedArray = snapshot.val().data;
const object = {
id: `${Math.random()}`,
images: uploadedImages,
};
update(changes, { data: [...fetchedArray, object] });
}
});
}

How to show loader during multiple api calls in a loop in React

I have two API's
First API returns list of items which I am iterating to get each item's detailed data.
Here's the code
const [loader, setLoader] = useState(false);
React.useEffect(() => {
const fetchUsers = async() => {
setLoader(true);
const users = await getUsers();
const promises = users.map(async (user) => {
let userData = await getUsersDetailedData(user.userId);
return userData
});
let finalUsers = await Promise.all(promises);
setLoader(false);
}
fetchUsers();
}, [])
I am updating loader state before the api call and after call but it is not working.
Loader state is updating these many times and loader is not displaying
logs
Try it in this way,
React.useEffect(() => {
const fetchUsers = async() => {
const users = await getUsers();
const promises = users.map(async (user) => {
let userData = await getUsersDetailedData(user.userId);
return userData
});
let finalUsers = Promise.all(promises);
return finalUsers;
}
setLoader(true);
fetchUsers().then(res=>{
setLoader(false);
});
}, [])

How to optimally combine multiple axios responses

I am working with a React app. I have to create 2 objects using responses from 3 different APIs. For example:
DataObject1 will be created using API1 and API2
DataObject2 will be created using API1, API2, and API3
So, I am thinking about what would be the most optimal way of doing this by making sure 1 call each API only once.
I was thinking this:
const requestOne = axios.get(API1);
const requestTwo = axios.get(API2);
const requestThree = axios.get(API3);
axios.all([requestOne, requestTwo, requestThree]).then(axios.spread((...responses) => {
const dataObject1 = createDataObject1(responses[0], responses[1]);
const dataObject2 = createDataObject2(responses[0], responses[1], responses[2]);
// use/access the results
})).catch(errors => {
// react on errors.
})
const createDataObject1 = (response1, response2) => { //Combine required data and return dataObject1 }
const createDataObject2 = (response1, response2, response3) => { //Combine required data and return dataObject2 }
Is there a better way of doing this?
Looks fine.
You can change this
axios.all([requestOne, requestTwo, requestThree]).then(axios.spread((...responses) => {
const dataObject1 = createDataObject1(responses[0], responses[1]);
const dataObject2 = createDataObject2(responses[0], responses[1], responses[2]);
// use/access the results
})).catch(errors => {
// react on errors.
})
to
axios.all([requestOne, requestTwo, requestThree]).then((response) => {
const dataObject1 = createDataObject1(responses[0], responses[1]);
const dataObject2 = createDataObject2(responses[0], responses[1], responses[2]);
// use/access the results
}).catch(errors => {
// react on errors.
})
because it is unnecessary to spread and rest.
If you don't want to use them like responses[0], responses[1], etc then you can use:
axios.all([requestOne, requestTwo, requestThree]).then(axios.spread((response1, response2, response3) => {
const dataObject1 = createDataObject1(response1, response2);
const dataObject2 = createDataObject2(response1, response2,response3);
// use/access the results
})).catch(errors => {
// react on errors.
})
Are you using thunk middleware to make async calls in Redux? I don't want to assume that you are, but that seems like a good basic approach here.
const requestOne = axios.get(API1);
const requestTwo = axios.get(API2);
const requestThree = axios.get(API3);
Okay. So now requestOne.data has the result of making the axios get request. Or, would if the thunk creator was async and the code was const requestOne = await axios.get(API1);
Do you need to parse the data further from request___.data ?
If not you can just have
const dataObj1 = { response1: requestOne.data, response2: requestTwo.data }
const dataObj2 = { ... dataObject1, response3: requestThree.data };
Full answer:
// store/yourFile.js code
export const YourThunkCreator = () => async dispatch => {
try {
const const requestOne = await axios.get(API1);
// other axios requests
const dataObj1 = { response1: requestOne.data, response2: requestTwo.data }
const dataObj2 = { ... dataObject1, response3: requestThree.data };
// other code
dispatch(// to Redux Store);
} catch (error) {
console.error(error);
}

use date from fetch object to create link for fetch another object in react

I write some code and works but i think maybe can be done on some better way. What i want from code? I create link and fetch object from that object i want use some value and pass that value in another link after that fetch new object. My code working but i want see if is possible new solution.
const [key, setKey] = useState("");
const [data, setData] = useState([]);
useEffect(() => {
getKey();
getWeather();
},[key]);
//this function get key from object and that key i will use in another link
const getKey = () => {
navigator.geolocation.getCurrentPosition(
(position) => {
const long = JSON.stringify(position.coords.longitude);
const lat = JSON.stringify(position.coords.latitude);
const proxy = `https://cors-anywhere.herokuapp.com/`;
const link = `${proxy}http://dataservice.accuweather.com/locations/v1/cities/geoposition/search?apikey=rhlYEhvAu0nhFNMFybOIhffbmjFX0AZN&q=${lat}%2C${long}&details=true`;
(async function fetchData(){
const getValue = await fetch (link);
const key = await getValue.json();
setKey(key.Key);
})();
}
);
};
const getWeather = async () => {
const proxy = `https://cors-anywhere.herokuapp.com/`;
const link = `${proxy}http://dataservice.accuweather.com/forecasts/v1/daily/5day/${key}?apikey=rhlYEhvAu0nhFNMFybOIhffbmjFX0AZN&details=true&metric=true`;
const data = await fetch (link);
const getData = await data.json();
setData(getData);
};
You can make this work by just making a few slight changes to your code. Make the useEffect and async function, return the key from getKey to a variable and await the variable assignment and pass to getWeather. Something like this:
const [key, setKey] = useState("");
const [data, setData] = useState([]);
useEffect(async() => { // <---- Converted to async
const apiKey = getKey(); // <---- Assigned to variable
getWeather(await apiKey); // <--- Using apiKey in function rather than just state
},[key]);
const getKey = () => {
navigator.geolocation.getCurrentPosition(
(position) => {
const long = JSON.stringify(position.coords.longitude);
const lat = JSON.stringify(position.coords.latitude);
const proxy = `https://cors-anywhere.herokuapp.com/`;
const link = `${proxy}http://dataservice.accuweather.com/locations/v1/cities/geoposition/search?apikey=rhlYEhvAu0nhFNMFybOIhffbmjFX0AZN&q=${lat}%2C${long}&details=true`;
(async function fetchData(){
const getValue = await fetch (link);
const key = await getValue.json();
setKey(key.Key);
return key.Key //<------ returned key for useEffect
})();
}
);
};
const getWeather = async (apiKey = key) => { // <----If no value passed to function, will use state value
const proxy = `https://cors-anywhere.herokuapp.com/`;
const link = `${proxy}http://dataservice.accuweather.com/forecasts/v1/daily/5day/${apiKey}?apikey=rhlYEhvAu0nhFNMFybOIhffbmjFX0AZN&details=true&metric=true`;
const data = await fetch (link);
const getData = await data.json();
setData(getData);
};
The reason I returned the value rather than using state is because setting state is asynchronous and there is currently no callback function for the useState setting function like there was for setState.

Categories