apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.32k stars 2.65k forks source link

ApolloClient: mutate Promise resolves before mutation takes place #9915

Open rwilliams3088 opened 2 years ago

rwilliams3088 commented 2 years ago

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 commented 2 years ago

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 */ }
);
jpvajda commented 2 years ago

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

rwilliams3088 commented 2 years ago

@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?

jpvajda commented 2 years ago

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

rwilliams3088 commented 1 year ago

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