How to call mutate only once on page visit? (For Change Email Confirmation Links)

I'm struggling to get a mutation to fire exactly once in my application. Here's the scenario: when a user decides to reset their email, a token with a random UUID is generated and stored in our database. The new email then receives a confirmation link like base-url.com/email-reset?token={token_uuid}. The user clicks the link and lands on a page that shows a modal, which varies based on whether it's loading, successful, or encounters an error. My goal is to trigger the resetEmail mutation only once, as soon as the token from the URL becomes available. However, I've noticed that the mutation often gets triggered multiple times. This is an issue because the first successful mutation deletes the token from the backend, causing any subsequent mutations to fail and display the error modal although the email change is successful. Here's a code snippet for context:
// By default, a loading modal is displayed
const resetEmail = api.emailReset.resetEmail.useMutation({
onSuccess: () => showSuccessModal(),
onError: () => showErrorModal(),
});
useEffect(() => {
if (query.token && resetEmail.isIdle) {
resetEmail.mutate({ id: query.token });
}
}, [query.token, resetEmail]);
// By default, a loading modal is displayed
const resetEmail = api.emailReset.resetEmail.useMutation({
onSuccess: () => showSuccessModal(),
onError: () => showErrorModal(),
});
useEffect(() => {
if (query.token && resetEmail.isIdle) {
resetEmail.mutate({ id: query.token });
}
}, [query.token, resetEmail]);
I've tried using useState to set a resetEmailTried flag, but that hasn't solved the problem. I suspect this is because React's state doesn't update instantly. Any help or insights would be much appreciated 🙏
5 Replies
Josh
Josh13mo ago
use a react ref that is initially false and set it to true inside your use effect after you send the query and then add a check for it being false to your if statement
// By default, a loading modal is displayed
const hasSent = useRef(false)
const resetEmail = api.emailReset.resetEmail.useMutation({
onSuccess: () => showSuccessModal(),
onError: () => showErrorModal(),
});
useEffect(() => {
// also useing happy path.
if(!query.token) return
if(!resetEmail.isIdle) return
if(hasSent.current) return
resetEmail.mutate({ id: query.token });
hasSent.current = true
}, [query.token, resetEmail]);
// By default, a loading modal is displayed
const hasSent = useRef(false)
const resetEmail = api.emailReset.resetEmail.useMutation({
onSuccess: () => showSuccessModal(),
onError: () => showErrorModal(),
});
useEffect(() => {
// also useing happy path.
if(!query.token) return
if(!resetEmail.isIdle) return
if(hasSent.current) return
resetEmail.mutate({ id: query.token });
hasSent.current = true
}, [query.token, resetEmail]);
happy path is just for readability, and a pattern ive grown to love and highly reccomend since hasSent is a ref, we dont need to put it in the dep array
clemwo
clemwo13mo ago
Thanks for the quick reply! I'll give that a try. And I did a quick search on happy path "pattern" (hard to call it pattern, more like code style, right?). Looks super intriguing and logical, you might have convinced me Is definitely related to not trying to nest code too much which I read about some time ago and have naturally tried to implement for a longer than that https://medium.com/codex/why-you-shouldnt-nest-your-code-185cf2e2cde3
Medium
Why You Shouldn't Nest Your Code
And how to get rid of nested code
clemwo
clemwo13mo ago
Doesn't work unfortunately. Here's my code:
const emailResetSent = useRef(false);

useEffect(() => {
if (query.token && resetEmail.isIdle && !emailResetSent.current) {
resetEmail.mutate({ id: query.token });
emailResetSent.current = true;
}
}, [query.token, resetEmail]);
const emailResetSent = useRef(false);

useEffect(() => {
if (query.token && resetEmail.isIdle && !emailResetSent.current) {
resetEmail.mutate({ id: query.token });
emailResetSent.current = true;
}
}, [query.token, resetEmail]);
if I add a console.log(emailResetSent.current); directly above the mutate I still can see two mutations happening and false being printed twice
Josh
Josh13mo ago
that would be because your probably in react strict mode which fully mounts and unmounts your component twice during development to make sure that your hooks are properly made try running in production mode, or turn off strict mode and you should see it work
clemwo
clemwo13mo ago
Ok, I think I caught the issue. I have a Layout which contains a sidebar that fetches and displays data. Apparently this causes my component to render twice. If I comment out the sidebar the issue disappears. Thanks for the help, I really appreciate that!
Want results from more Discord servers?
Add your server