query: useMutation adds undefined to TResult of MutateFunction type

Describe the bug

useMutation changes the return type of provided promise by adding undefined to it.

To Reproduce

const apiCall = () => new Promise<string>(resolve => resolve());

const useExample = async () => {
	const [mutateCall] = useMutation(apiCall);
	const response = await apiCall(); // string
	const mutateR = await mutateCall(); // string | undefined
};

Wrapping a function in useMutation adds a undefined to it’s returned promise

Expected behavior

The provided function’s return type should not be changed.

Desktop (please complete the following information):

  • Version: 23

Additional context

I don’t see any reason why this would be required. Together with #1077 this is making migration to this library pretty unnecessarily painful by imposing this opinionated types.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 22

Most upvoted comments

I think the PR above fixes this issue. v3 will be awesome!

What about providing two mutate functions? One for callbacks and for promises?

Callback version:

const { mutate } = useMutation(apiMutate)

mutate({
  onSuccess: result => {
    console.log(result)
  },
  onError: error => {
    console.error(error)
  },
});

Promise version:

const { mutateAsync } = useMutation(apiMutate)

try {
  const result = await mutateAsync()
  console.log(result)
} catch (error) {
  console.error(error)
}

Then mutate would return nothing, mutateAsync would return a promise and throwOnError can be removed.

@TkDodo the full example is a little more complex. I am using Formik, which allows to track an isSubmitting state:

Inside MyForm, myApiCall is mutate:

    <Formik
      onSubmit={(values, { setSubmitting }) => {
        callApi(values)
          .then(() => setSubmitting(true))
          .finally(() => setSubmitting(false))
      }}
    >
      {
        ({ isSubmitting }) => (
          <form>
            { /* form fields  */ }
            <button type='submit' disabled={isSubmitting}>Save</button>     
          </form>
        )
      }
    </Formik> 

and in the parent component I can just have:

const [mutate] = useMutation(..., { throwOnError: true });

return <MyForm callApi={mutate} .... />

This allows me to rely on react-query AND keep the loading logic in the child form component. I could do it by exposing formik helpers:

<MyForm
      onSubmit={(values, { setSubmitting }) => mutate(values, {
        onSuccess: () => { setSubmitting(false) },
        onError: () => { setSubmitting(false) }
      })}
 />

But I don’t like this, since I feel it breaks encapsulation. My parent component should not really know about formik helpers.

Honestly, I really do not see the advantage of the callbacks.

@simonedavico did you read my comment? If you want to make sure that you are in the success state, I think you should use the onSuccess callback:

const apiCall = () => new Promise<string>(resolve => resolve());

const useExample = async () => {
    const [mutateCall] = useMutation(apiCall);
    await mutateCall({
        onSuccess: (mutateR) => { // mutateR will be of type string here ... }
    }); 
};

Unfortunately not everything can be done in the onSuccess callback, for interop with other code I need from time to time to access the Promise result. I believe that the | undefined union should be added only when throwOnError is false; if throwOnError is true, the result can never be undefined if the promise resolves.

Hi @Haaxor1689! Issue #1077 will be fixed in V3 and this type is actually correct because the mutate function by default returns undefined when the mutation fails