How to use async await in React components without useEffect - javascript

My app has a search bar where the user types in a word and clicks search. Upon click, the app fetches the definition of that word from an open dictionary API, and then updates the parent component's state to reflect the results. The component is then supposed to render the results by passing that as a prop to a presentational child component.
However, it looks like the state gets set before the fetch call has the time to return the data. So the search results are not rendered until the search button is clicked again, causing confusion.
I have tried resolving this by making my function asynchronous (Dictionary.search refers to an imported function which handles the fetch and returns the result in the form of an array):
async search(term) {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
}
However this only works some of the time. For longer searches, it doesn't wait before updating the state and re-rendering.
I have done some googling and came across this suggestion, but it also doesn't work:
search = (term) => {
const dictionarySearch = async (term) => {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
};
dictionarySearch(term);
};
EDITED TO ADD: Here is the Dictionary.search code, along with its helper function:
//Create an empty array in which the results of the API call will be stored
let results = [];
const Dictionary = {
search(term) {
//Url with our search term and API key:
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
//Fetch the results from Merriam Webster API:
fetch(url)
.then((response) => {
//Catch an error from their server
if (!response.ok) {
throw new Error("Network response was not ok");
}
//Return javaScript object version of the response
return response.json();
})
.then((jsonResponse) => {
//Perform the helper function on the javascript object and return the result (array)
return shortDef(jsonResponse);
})
//Catch any other errors than a server error
.catch((error) => {
console.error(
"There has been a problem with your fetch operation:",
error
);
});
//Create a copy of the results array
let returnResults = results.slice();
//Reset the original array to an empty array
results = [];
//Return the copy
return returnResults;
},
};
//Helper function to extract only certain info from the API
function shortDef(response) {
response.forEach((object) => {
//Create a new object for each object int he response array
let result = { word: "", type: "", definitions: [] };
//Add the word and type to the object
result.word = object.hwi.hw;
result.type = object.fl;
//Add the definitions to the object. There may be several, so it is pushed to an array.
let defs = object.shortdef;
defs.forEach((def) => {
result.definitions.push(def);
});
//Add the object to the array of API results
results.push(result);
});
//Return the list of results
return results;
}
I don't want to call the API in the ComponentDidMount, because it should get called every time the user presses "search". I also would prefer not to use useEffect, as it would mean refactoring my entire component from a class to a function.
Is there no way to have the setState in a class component wait for an asynchronous task to complete?

The problem is that your Dictionary.search function immediately returns, because it does not wait until the .then block resolves. Change it to an async function and await the fetch of the url. It should look like this:
const Dictionary = {
// Make search an async function
search: async term => {
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
// Await the results
await fetch(url)
.then(response => {
// ...
})
.then(jsonResponse => {
// ...
})
.catch(error => {
// ...
});
return;
},
};

Related

I query firestore for a document and try to add it to an array, but the array returns empty

I am trying to get a user from a document meant to hold user data. The document holds this data
First: "Josh"
Last: "Solders"
email: "example#gmail.com"
phone: "7865572525"
superAdmin: "on"
userID: "admin-1"
I run this function:
export const getAdmins = functions.https.onCall(async (data, context) => {
var admins: FirebaseFirestore.DocumentData[] = [];
admin.firestore().collection("admin").get().then((querySnapshot) => {
var c = 0;
querySnapshot.forEach((doc) => {
admins[c] = doc.data();
c++;
console.log(doc);
console.log(admins[c]);
});
});
return admins;
})
and it returns to here:
const getAdmins = firebase.functions().httpsCallable('getAdmins');
// Passing params to data object in Cloud functinon
getAdmins({}).then((results) => {
admins = results;
console.log("admins retrieved");
console.log(results);
});
The function returns and the log shows that it accessed the file, however, the array returns empty. I am not quite sure why this isn't returning properly unless the local variable admins can't be accessed once its inside get call. I don't think that makes sense, but that is the best explanation for it myself. I could use some help figuring out this issue.
The return statement may run even before your promise is resolved as it's asynchronous. Your function is async so try using await instead of Promise chaining:
export const getAdmins = functions.https.onCall(async (data, context) => {
const querySnapshot = await admin.firestore().collection("admin").get()
return querySnapshot.docs.map((doc) => doc.data())
})
Also you can use map method instead to simplify the code.

React: String automatically converted to [object promise] when called from another component

I'm developing the front-end for my spring boot application. I set up an initial call wrapped in a useEffect() React.js function:
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
'/myapi/' + auth.authState.id
);
setData(data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
The data returned isn't comprehensive, and needs further call to retrieve other piece of information, for example this initial call return an employee id, but if I want to retrieve his name and display it I need a sub-sequential call, and here I'm experiencing tons of issues:
const getEmployeeName = async id => {
try {
const name = await fetchContext.authAxios.get(
'/employeeName/' + id
);
console.log((name["data"])); // <= Correctly display the name
return name["data"]; // return an [Object promise],
} catch (err) {
console.log(err);
}
};
I tried to wrap the return call inside a Promise.resolve() function, but didn't solve the problem. Upon reading to similar questions here on stackoverflow, most of the answers suggested to create a callback function or use the await keyword (as I've done), but unfortunately didn't solve the issue. I admit that this may not be the most elegant way to do it, as I'm still learning JS/React I'm open to suggestions on how to improve the api calls.
var output = Object.values(data).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Async functions always return promises. Any code that needs to interact with the value needs to either call .then on the promise, or be in an async function and await the promise.
In your case, you should just need to move your code into the existing useEffect, and setState when you're done. I'm assuming that the employeeID is part of the data returned by the first fetch:
const [name, setName] = useState('');
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
"/myapi/" + auth.authState.id
);
setData(data);
const name = await fetchContext.authAxios.get(
'/employeeName/' + data.employeeID
);
setName(name.data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
// ...
var output = Object.values(appointmentsData).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Note that the above code will do a rerender once it has the data (but no name), and another later when you have the name. If you want to wait until both fetches are complete, simply move the setData(data) down next to the setName

Why is my asynchronous input undefined in useEffect?

I'm writing a React application that fetches image data from a server for an array of URLs. I am storing the camera images as large strings that are placed into the image's src attribute. I am using useReducer to store my dictionary of camera objects.
I am having a couple of problems getting the reducer to work, and one of them has to do with some confusion I'm having with asynchronous values and why the async function returns correct output but the completion handler (.then()) receives undefined as a result.
Here is the code for useEffect() and the asynchronous fetching function.
useEffect()
//Why is cameras undefined?
useEffect(() => {
if (phase === 0) {
let cameras = {}
getCameraInformation().then((cameras) => {
debugger;
dispatch({
type: 'loadedCameraInformation',
payload: {cameras: cameras}
});
}).finally(() => setPhase(1))
}
});
My function signature and variables:
export default function Main() {
const [state, dispatch] = useReducer(cameraReducer, initialState);
let [phase, setPhase] = useState(0);
My function for getCameraInformation:
This returns a dictionary full of correct information!
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let cam_json = await axios
.get(getCamerasURL, { headers: { auth: get_cookie("token") } })
.then(response => {
let tempCameraArray = response.data.body;
let tempCameraDictionary = {};
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null
};
}
return tempCameraDictionary;
})
.catch(error => console.log(error));
}
Your async function getCameraInformation doesn't have a return statement, so its promise will not resolve any value. There is a return in the then callback, but that's a different function entirely.
You are also using await and then() on the same promise, which isn't ideal. Use one or the other, because it's very easy to get confused when you mix and match here.
You already have an async, so don't use then at all in side that function.
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let response = await axios.get(getCamerasURL, { headers: { auth: get_cookie('token') } })
let tempCameraArray = response.data.body
let tempCameraDictionary = {}
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null,
}
}
return tempCameraDictionary
}

Multiple Async call resolutions using Promises

In Node.js, I have a Promise.all(array) resolution with a resulting value that I need to combine with the results of another asynchronous function call. I am having problems getting the results of this second function, since it resolves later than the promise.all. I could add it to the Promise.all, but it would ruin my algorithm. Is there a way to get these values outside of their resolutions so I can modify them statically? Can I create a container that waits for their results?
To be more specific, I am reading from a Firebase realtime database that has been polling API data. I need to run an algorithm on this data and store it in a MongoDB archive. But the archive opens aynchronously and I can't get it to open before my results resolve (which need to be written).
Example:
module.exports = {
news: async function getNews() {
try {
const response = await axios.get('https://cryptopanic.com/api/posts/?auth_token=518dacbc2f54788fcbd9e182521851725a09b4fa&public=true');
//console.log(response.data.results);
var news = [];
response.data.results.forEach((results) => {
news.push(results.title);
news.push(results.published_at);
news.push(results.url);
});
console.log(news);
return news;
} catch (error) {
console.error(error);
}
},
coins: async function resolution() {
await Promise.all(firebasePromise).then((values) => {
//code
return value
}
}
I have tried the first solution, and it works for the first entry, but I may be writing my async function wrong on my second export, because it returns undefined.
You can return a Promise from getNews
module.exports = {
news: function getNews() {
return axios.get('https://cryptopanic.com/api/posts/?auth_token=518dacbc2f54788fcbd9e182521851725a09b4fa&public=true')
.then(res => {
// Do your stuff
return res;
})
}
}
and then
let promiseSlowest = news();
let promiseOther1 = willResolveSoon1();
let promiseOther2 = willResolveSoon2();
let promiseOther3 = willResolveSoon3();
Promise.all([ promiseOther1, promiseOther2, promiseOther3 ]).then([data1,
data2, data3] => {
promiseSlowest.then(lastData => {
// data1, data2, data3, lastData all will be defined here
})
})
The benefit here is all promises will start and run concurrently so your total waiting time will be equal to the time taken by the promiseSlowest.
Check the link for more:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#Examples

Render array of objects in React

I have a method in a React component which fetches some data from an API
this.state.matches returns an empty array at first
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
matches.push(response.data)
})
});
this.setState({
matches
})
}
And then a method which should map the data to React components
renderMatch() {
return this.state.matches.map((match, index) => {
return (
<Match
key={index}
gameId={match.gameId}
/>
);
});
}
renderMatch() is called in my render method with {this.renderMatch()}
But nothing is getting rendered and if i call .length it just returns 0 even though i can see in devtools that the array contains 5 objects.
Hardcoded objects in the array gets rendered
You are mutating the state so React doesn't trigger a new render. You should create a new array instead of pushing in the state :
loadMatches() {
let promises = [];
forEach(this.state.matchIds.splice(0,5), matchid => {
promises.push(axios.get(url).then(res => res.data));
});
Promise.all(promises).then(matches => {
this.setState({
matches
});
});
}
Edited to handle the async.
axios or fetch is an async function, there for when you call this.setState matches is still an empty array, hence nothing is rendered. try this instead. also you are trying to mutate the state directly which is a big mistake.
loadMatches() {
let matches = this.state.matches;
let newMatches =[];
let requests = forEach(this.state.matchIds.splice(0,5), matchid => {
return new Promise((resolve) => {
axios.get(url)
.then(function (response) {
newMatches.push(response.data)
})
});
});
Promise.all(requests).then(() => {
this.setState({
matches:newMatches
})
});
}
You have two main problems:
First:
.then(function (response) {
matches.push(response.data)
})
You should not change state directly, you should perform immutable update.
Second:
this.setState({
matches
})
You are updating state with empty 'matches', at this point your api call's responses are not received yet.
So, your implementation should look like this:
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
const newMatch = response.data;
this.setState({ matches: [...matches, newMatch]});
matches.push(response.data)
})
});
}
Here some sources that you can also benefit from reading them:
https://daveceddia.com/immutable-updates-react-redux/
https://reactjs.org/docs/state-and-lifecycle.html
axios.get is an asynchronous function which returns a promise. Its like you've ordered a pizza and trying to eat it before it gets delivered. You need to perform the setState() when you resolve the promise.
axios.get(url)
.then(function (response) {
this.setState({matches: [...matches, ...response.data]})
}

Categories