T
TanStack9mo ago
foreign-sapphire

Using component state values in useMutation.mutate (closure/race condition help)

Can someone help me understand why my payload is always an empty string in the code below? I have a state variable that holds a user's comment which works fine. When a button is pressed I call useMutation.mutate() and then reset the comment state. Inside the mutationFn the value of comment is always the reset state and it seems like the setState function executes before the mutationFn even if it's called afterward. I know I can fix this by passing an argument to the mutationFn or resetting the state as part of an onSuccess callback. I know there's better practices than the code below. But what I really want is some help understanding why the comment is the reset value. Why does the mutation seem to execute after the state is updated?
const Feedback = () => {
const [comment, setComment] = useState('');

const postCommentMutation = useMutation({
mutationFn: () => {
const postComment = async () => {
const payload = comment; // Why is this always an empty string?
return await myApi.postComment(payload);
};
return postComment();
},
});

return (
<>
<input value={comment} onChange={e => setComment(e.target.value)} />
<button
onClick={() => {
postCommentMutation.mutate();
setComment('');
}}
/>
</>
);
};
const Feedback = () => {
const [comment, setComment] = useState('');

const postCommentMutation = useMutation({
mutationFn: () => {
const postComment = async () => {
const payload = comment; // Why is this always an empty string?
return await myApi.postComment(payload);
};
return postComment();
},
});

return (
<>
<input value={comment} onChange={e => setComment(e.target.value)} />
<button
onClick={() => {
postCommentMutation.mutate();
setComment('');
}}
/>
</>
);
};
3 Replies
absent-sapphire
absent-sapphire9mo ago
Pass it to mutate and clear it in onSuccess.
const postCommentMutation = useMutation({
mutationFn: (comment: string) => {
...
},
onSuccess: () => {
setComment('')
}
})
const postCommentMutation = useMutation({
mutationFn: (comment: string) => {
...
},
onSuccess: () => {
setComment('')
}
})
Then the click becomes:
onClick={(): void => {
postCommentMutation.mutate(comment)
}}
onClick={(): void => {
postCommentMutation.mutate(comment)
}}
Mutate isn't awaitable so what's likely happening is you end up scheduling both the mutate and the state update at the same time. Dominik goes into detail about stuff like this in his blog. https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose
Breaking React Query's API on purpose
Why good API design matters, even if it means breaking existing APIs in the face of resistance.
absent-sapphire
absent-sapphire9mo ago
Technically you could await mutateAsync but I think the callback approach is more common.
onClick={async (): Promise<void> => {
await postCommentMutation.mutateAsync()
setComment('')
}}
onClick={async (): Promise<void> => {
await postCommentMutation.mutateAsync()
setComment('')
}}
I'm a believer that your mutation should have everything given to it, i.e. pass comment to the mutation, not have it reach out for anything. Doing so also allows the devtools to keep track of your mutation payloads. Otherwise it can't know what your request consisted of
foreign-sapphire
foreign-sapphireOP8mo ago
Thank you! These comments helped me get to the bottom of things!

Did you find this page helpful?