apollo-client
apollo-client copied to clipboard
ApolloClient: mutate Promise resolves before mutation takes place
Intended outcome: I am attempting to use the ApolloClient in combination with react-hook-form to submit a mutation, wait for it to succeed/fail, and respond accordingly.
Actual outcome: The mutate function resolves successfully immediately after being invoked - regardless of whether or not the mutation request succeeded. There's no easy way for me to capture and respond to errors as part of the form submission process because of this.
How to reproduce the issue:
const { handleSubmit } = useForm();
const client = useApolloClient();
const onSubmit = (data, e) => {
return client.mutate(...).then(
response => { ..., },
err => { ... } // <--- never gets triggered
);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
);
Versions
System:
OS: Linux 5.10 Ubuntu 20.04.4 LTS (Focal Fossa)
Binaries:
Node: 15.14.0 - ~/.nvm/versions/node/v15.14.0/bin/node
Yarn: 3.1.1 - ~/repos/fifthsun/web/node_modules/.bin/yarn
npm: 7.7.6 - ~/.nvm/versions/node/v15.14.0/bin/npm
Browsers:
Chrome: 102.0.5005.61
npmPackages:
@apollo/client: ^3.3.6 => 3.6.9
@apollo/react-hoc: ^4.0.0 => 4.0.0
apollo: ^2.32.1 => 2.34.0
apollo-link-scalars: ^3.0.0 => 3.0.0
I went ahead and wrote up the following hook as a work-around that seems to do the trick:
import React, { useEffect, useState } from "react";
import {
ApolloCache, DefaultContext, DocumentNode, MutationFunctionOptions, MutationHookOptions,
MutationTuple, OperationVariables, TypedDocumentNode, useMutation
} from "@apollo/client";
export type DeferredResolver<TData> = (data: TData) => void;
export type DeferredRejector = (error?: any) => void;
export type Deferred<TData> = { resolve: DeferredResolver<TData>, reject: DeferredRejector };
export function useMutationAndWait<TData = any, TVariables = OperationVariables,
TContext = DefaultContext, TCache extends ApolloCache<any> = ApolloCache<any>>
(mutation: DocumentNode | TypedDocumentNode<TData, TVariables>,
options?: MutationHookOptions<TData, TVariables, TContext>): MutationTuple<TData, TVariables, TContext, TCache> {
const [_mutate, results] = useMutation<TData, TVariables, TContext, TCache>(mutation, options);
const [deferred, setDeferred] = useState<Deferred<TData>>();
if(deferred && !results.loading) {
if(results.error) {
deferred.reject(results.error);
} else {
deferred.resolve(results.data!);
}
setDeferred(undefined);
}
const mutate = (_options?: MutationFunctionOptions<TData, TVariables, TContext, TCache>) =>
_mutate(_options)
.then(_ => new Promise<TData>((resolve, reject) => {
setDeferred({ resolve, reject });
}));
return [ mutate, results ];
};
export default useMutationAndWait;
You can use it the same way as the regular useMutation hook, except that now the promise won't resolve/reject until it actually has the results.
const [update] = useMutationAndWait<MyData, MyVars>(...);
...
update(...).then(
(data: MyData) => { /* this will only get called with the actual results */ },
(reason: any) => { /* this will actually get called on error now */ }
);
@rwilliams3088 clever solution, do you feel this issue is resolved now?
@jpvajda Thanks :) This has been working for me. However, I feel that this should really be baked into the ApolloClient as standard functionality; preferably as part of the existing useMutation hook (perhaps controlled by an optional parameter).
Or else, what would be the recommended approach for handling errors within the context of a form like this using the vanilla operations offered by the ApolloClient?
@rwilliams3088 I agree, it be nice to offer this in the client.
Update: One of the updates between my solution and the present version has broken the above so that it no longer serves to fix the problem... So this is a bug again