const response = yield call(fetch, `${config.backendUrl}/verify`, {
method: 'POST'
})
const responseJson = yield call(response.json)
console.log(responseJson)
This is code from redux-saga. Yield hangs and console.log does not print anything. But if I replace response.json with () => response.json() it works. Why?
That's because when you invoke yield call(response.json) the response.json is called with wrong context (this).
You could fix this with bind (e.g. yield call(response.json.bind(response))), or by specifying context (e.g. yield call([response, response.json])), but call here is really useless. You can just:
const responseJson = yield response.json();
Related
I am trying to store the downloadLink from firebase's storage into firestore. I am able to set all the data, and I am able to set the link, the second time I click the "post" button.
I know the issue has to do with asynchronous functions, but I'm not experienced enough to know how to solve the issue.
In the "createPost" function, I am console logging "i am the URL: {url}" and in the "uploadFile" function, I am console logging "look at me {url}" to debug.
I noticed the "I am the URL" outputs nothing and then shortly after, the "look at me" outputs the URL.
setDoc() of course stores the imageLink as an empty string.
What can I do to solve this? Any help would be greatly appreciated or any documentation to help with my understanding of async functions.
Here is my code:
const PostModal = (props) => {
const makeid = (length) => {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const [descriptionText, setDescriptionText] = useState("");
const [addressText, setAddressText] = useState("");
const [venueText, setVenueText] = useState("");
const [startTimeText, setStartTimeText] = useState("");
const [endTimeText, setEndTimeText] = useState("");
const [shareImage, setShareImage] = useState("");
const [videoLink, setVideoLink] = useState("");
const [assetArea, setAssetArea] = useState("");
const [url, setURL] = useState("");
const { data } = useSession();
const storage = getStorage();
const storageRef = ref(storage, `images/${makeid(5) + shareImage.name}`);
const uploadFile = () => {
if (shareImage == null) return;
uploadBytes(storageRef, shareImage).then( (snapshot) => {
//console.log("Image uploaded")
getDownloadURL(snapshot.ref).then( (URL) =>
{
setURL(URL);
console.log(`look at me: ${URL}`)});
});
}
const createPost = async () => {
var idLength = makeid(25);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadFile()
console.log(`I am the URL: ${url} `)
setDoc(doc(db, "posts", idLength), {
eventDescription: descriptionText,
eventAddress: addressText,
venueName: venueText,
startTime: startTimeText,
endTime: endTimeText,
imageLink: url,
videoLink: videoLink,
username: data.user.name,
companyName: !data.user.company ? "" : data.user.company,
timestamp: Timestamp.now(),
});
}
const handleChange = (e) => {
const image = e.target.files[0];
if(image === '' || image === undefined) {
alert('not an image, the file is a ${typeof image}');
return;
}
setShareImage(image);
};
const switchAssetArea = (area) => {
setShareImage("");
setVideoLink("");
setAssetArea(area);
};
const reset = (e) => {
setDescriptionText("");
setAddressText("");
setVenueText("");
setStartTimeText("");
setEndTimeText("");
setShareImage("");
setVideoLink("");
setURL("");
props.handleClick(e);
};
This was taken from a reddit user who solved my answer. Big thank you to him for taking the time to write out a thoughtful response.
So, you're kinda right that your issue has a bit to do with asynchronicity, but it's actually got nothing to do with your functions being async, and everything to do with how useState works.
Suffice it to say, when you call uploadFile in the middle of your createPost function, on the next line the value of url has not yet changed. This would still be true even if uploadFile were synchronous, because when you call a useState setter function, in this case setURL, the getter value url doesn't change until the next time the component renders.
This actually makes perfect sense if you stop thinking about it as a React component for a moment, and imagine that this was just vanilla JavaScript:
someFunction () {
const url = 'https://www.website.com';
console.log(url);
anotherFunction();
yetAnotherFunction();
evenMoreFunction();
console.log(url);
}
In this example, would you ever expect the value of url to change? Probably not, since url is declared as const, which means if the code runs literally at all, it's physically impossible for the value of url to change within a single invocation of someFunction.
Functional components and hooks are the same; in a single "invocation" (render) of a functional component, url will have the same value at every point in your code, and it's not until the entire functional component re-renders that any calls to setURL would take effect.
This is an extremely common misunderstanding; you're not the first and you won't be the last. Usually, it's indicative of a design flaw in your data flow - why are you storing url in a useState to begin with? If you don't need it to persist across distinct, uncoupled events, it's probably better to treat it like a regular JavaScript value.
Since uploadBytes returns a promise, you could make uploadFile asynchronous as well, and ultimately make uploadFile return the information you need back to createPost, like this:
const uploadFile = async () => {
if (shareImage == null) return;
const snapshot = await uploadBytes(storageRef, shareImage);
// console.log("Image uploaded")
const URL = await getDownloadURL(snapshot.ref);
return URL;
};
All I've done here us un-nest your .then calls, pulling the trapped values out into the usable scope of your uploadFile function. Now, you can change that one line of createPost to this:
const url = await uploadFile();
and eliminate your useState altogether.
I'm new in JS world and callbacks.
Why I can't return response after then function for the Firebase callable functions?
It returns empty if I return like shown below. I guess it doesn't wait for the response, response has data.output variable actually.
exports.testApi = functions.https.onCall(async(data, context) => {
const formData = new FormData();
formData.append("height", "512");
const response = await axios.post('https://....', formData, {
headers: formData.getHeaders()
})
.then((response) => {
console.log(response.data);
return {'imageURL':response.data.output};
})
.catch((error) => {
console.log(error)
});
}
);
It works with this format
const response = await axios.post('https://..', formData, {
headers: formData.getHeaders()
})
return {'imageURL':response.data.output};
The main attraction of async and await is cleaner syntax, in particular syntax that doesn't use then (opinion). You can mix them but I would always try to avoid it, especially here where the task is so simple.
exports.testApi = functions.https.onCall(async(data, context) => {
try {
const formData = new FormData();
formData.append("height", "512");
// Wait for post to give us a response.
const response = await axios.post('https://....', formData, {
headers: formData.getHeaders()
});
// If we get here, we waited and got a response. Proceed.
console.log(response.data);
return {'imageURL': response.data.output};
} catch (error) {
// If we get here, post threw an error (assuming it throws).
// And we never executed any lines after const response = await...
console.log(error);
}
});
Side note: Firebase Cloud Functions have to be properly terminated and this function does not. For example, if post throws an error and control flows to the catch block then the function will simply timeout because we haven't returned a Promise or thrown a compliant error.
I have always been made to believe that async and await and promises were one and the same thing, here, in my code, res.json() is working with await but isnt working at all with .then(). maybe I am missing something. please this is not a duplicate question. I have been scouring the internet for answers for the most part of two days now.
Here is the code with .then, aync and await, and i will also include the code from my server that sends the json
CODE USING .then()
const requestURL = "/auth/profile";
const request = new Request(requestURL, {
method: 'POST'
});
fetch(request)
.then( (res) => {
res.json()
})
.then(obj => {
console.log(obj)
})
.catch(err => {
console.log(err)
})
here, console.log(obj) logs undefined
CODE USING ASYNC AND AWAIT
const requestURL = "/auth/profile";
const request = new Request(requestURL, {
method: 'POST'
});
fetch(request)
.then( aync (res) => {
const obj = await res.json()
console.log(obj)
})
.catch(err => {
console.log(err)
})
here, console.log(obj) logs the correct json object as expected
server request handler (nodejs)
const getProfile = async (req, res, next) => {
const result = JSON.stringify({"status":"not logged in", "body": "there is a big poblem"})
res.setHeader('content-type', 'application/json');
res.status(200).send(result)
}
While I have used other tools to make sure I have the response from my server properly set up, if i am missing something kindly indicate to me, cheers
The problem is in your handling of of the chains. You need to return data to the next then, it's not automatic and varies depending on scope. Change to:
fetch(request)
.then( (res) => {
// Process.. then return some data to the next chain in line.
return res.json()
})
.then(obj => {
// Now, obj will be what the line above 'returned'
console.log(obj)
})
.catch(err => {
console.log(err)
})
Trying to read a pushToken from a given user in the users collection (after an update operation on another collection) returns undefined
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
})
.then(r => {
getToken(idTo).then(token => {
// sendMsg...
})
}).catch(updateErr => {
console.log("updateErr: " + updateErr)
})
async function getToken(id) {
let response = "getTokenResponse"
console.log("id in getToken: " + id)
return db.collection('users').doc(id).get()
.then(user => {
console.log("user in getToken: " + user.data())
response = user.data().pushToken
})
.catch(e => {
console.log("error get userToken: " + e)
response = e
});
return response
}
return null
});
And this is from the FB console log:
-1:43:33.906 AM Function execution started
-1:43:36.799 AM Function execution took 2894 ms, finished with status: 'ok'
-1:43:43.797 AM id in getToken: Fm1RwJaVfmZoSgNEFHq4sbBgoEh1
-1:43:49.196 AM user in getToken: undefined
-1:43:49.196 AM error get userToken: TypeError: Cannot read property 'pushToken' of undefined
-1:43:49.196 AM returned token: undefined
And we can see in this screenshot from the db that the doc does exist:
Hope someone can point me to what I'm doing wrong here.
added screenshot of second example of #Renaud as deployed:
As Doug wrote in his comment, you need to "return a promise from the top level function that resolves when all the async work is complete". He also explains that very well in the official video series: https://firebase.google.com/docs/functions/video-series/ (in particular the 3 videos titled "Learn JavaScript Promises"). You should definitely watch them, highly recommended!
So, the following modifications to your code should work (untested):
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite(async (snap, context) => { // <- note the async keyword
try {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
await db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
});
const userToSnapshot = await db.collection('users').doc(idTo).get();
const token = userToSnapshot.data().pushToken;
await sendMsg(token); // <- Here you should take extra care to correctly deal with the asynchronous character of the sendMsg operation
return null; // <-- This return is key, in order to indicate to the Cloud Function platform that all the asynchronous work is done
} catch (error) {
console.log(error);
return null;
}
});
Since you use an async function in your code, I've used the async/await syntax but we could very well write it by chaining the promises with the then() method, as shown below.
Also, I am not sure, in your case, that it adds any value to put the code that gets the token in a function (unless you want to call it from other Cloud Functions but then you should move it out of the addDenuncia Cloud Function). That's why it has been replaced by two lines of code within the main try block.
Version with chaining promises via the then() method
In this version we chain the different promises returned by the asynchronous methods with the then() method. Compared to the async/await version above, it shows very clearly what means "to return a promise from the top level function that resolves when all the asynchronous work is complete".
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => { // <- no more async keyword
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
return db.collection('Classificados').doc(classificadoId) // <- we return a promise from the top level function
.update({
aprovado: false
})
.then(() => {
return db.collection('users').doc(idTo).get();
})
.then(userToSnapshot => {
if {!userToSnapshot.exists) {
throw new Error('No document for the idTo user');
}
const token = userToSnapshot.data().pushToken;
return sendMsg(token); // Again, here we make the assumption that sendMsg is an asynchronous function
})
.catch(error => {
console.log(error);
return null;
})
});
Is there some usual pattern to chain redux-saga async requests synchronously? Eg. 1 function loads user ID and the second call some API request using that ID. I will try to demonstrate (this code isn't solution, just demonstration)
function* laodUserSaga(action) {
try {
const res = yield apiGet('/user')
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileDataByUserID(action) {
const state = yield select();
try {
const res = yield apiGet(`/user/${state.userID}user-profile`)
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileWithDataSaga(aciton){
yield put(laodUserSaga)
yield put(loadProfileDataByUserID)
}
function* sagaConnect() {
yield all([
takeLatest(LOAD_USER, laodUserSaga),
takeLatest(LOAD_USER_DATA_BY_PROFILE_ID, loadProfileDataByUserID),
takeLatest(LOAD_USER_WITH_PROFILE, loadProfileWithDataSaga),
])
}
you see, such examples would be really useful in docs of every library, 1 real world example is often what I understand in seconds, am I the only one?
You don't have to start every saga using takeEvery/Latest helper.
You can just call sagas as any other function, e.g.:
function* loadProfileWithDataSaga() {
const user = yield call(loadUser)
const profileData = yield call(loadProfileDataByUserID, user.id)
}
You just need to write loadUser and loadProfileDataByUserID sagas so that they receive and return the right values. If you want to be still able to call them in other scenarios by dispatching actions I would create another sagas that would just wrap the loadUser/Profile functionality.