apollo-client icon indicating copy to clipboard operation
apollo-client copied to clipboard

ApolloClient: mutate Promise resolves before mutation takes place

Open rwilliams3088 opened this issue 2 years ago • 5 comments

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 

rwilliams3088 avatar Jul 16 '22 22:07 rwilliams3088

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 avatar Jul 17 '22 01:07 rwilliams3088

@rwilliams3088 clever solution, do you feel this issue is resolved now?

jpvajda avatar Jul 25 '22 20:07 jpvajda

@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 avatar Jul 31 '22 03:07 rwilliams3088

@rwilliams3088 I agree, it be nice to offer this in the client.

jpvajda avatar Aug 01 '22 19:08 jpvajda

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

rwilliams3088 avatar Sep 22 '22 22:09 rwilliams3088