apollographql / apollo-feature-requests

🧑‍🚀 Apollo Client Feature Requests | (no 🐛 please).
Other
130 stars 7 forks source link

Default onError/onCompleted callback #396

Open ijxy opened 1 year ago

ijxy commented 1 year ago

A little confused about whether this is the appropriate place, since this repo says to use the feature-request repo, but the feature-request repo says to make project-specific requests in the project:

image

What

With that out of the way, what I'd like to be able to do is to set a default implementation for the onError and onCompleted callback options for queries/mutations when instantiating the Apollo client.

Why

Convenience!

For example, consider an internal application that has a lot of different pages with lots of actions that users can take–let's just say >100 mutations to put some numbers to it. In this application, we want to let the user know when something didn't succeed but we don't really need to do anything fancy–just show the error message.

Currently, we have to add onError to every mutation individually, which requires devs to remember to do it in the first place and potentially make some other mistake. If we could just set a default onError, then we'd get the desired behaviour for every mutation by default!

How

There is already a defaultOptions with allows for setting such things as default fetch policies, etc, so perhaps that could be extended. Since this would be opt-in, it would not break existing users and so could be rolled out in a minor version update.

Example Usage

new ApolloClient({
  ...apolloClientConfig, // other stuff
  defaultOptions: {
    mutate: {
      onCompleted: (e) => window.alert("Completed successfully!"),
      onError: (e) => window.alert(`Failed: ${e.message}`),
    },
  },
});
jerelmiller commented 1 year ago

@jxdp would you provide some examples of the kinds of logic you'd use in those global onCompleted/onError callbacks? I suspect you're not using window.alert in a production application 🙂. Are you showing toast messages or doing some kind of logging in here? Any more info would be appreciated. Thanks!

jerelmiller commented 1 year ago

FYI, going to transfer this to the feature requests repo. I'll make sure to get our messaging updated on the other repo the make it less confusing. Thanks for pointing that out!

ijxy commented 1 year ago

@jxdp would you provide some examples of the kinds of logic you'd use in those global onCompleted/onError callbacks? I suspect you're not using window.alert in a production application 🙂. Are you showing toast messages or doing some kind of logging in here? Any more info would be appreciated. Thanks!

Hi @jerelmiller, thanks for your response and for moving this to the right place!

We'd love to be able to do set a default that includes some basic logic for logging, capture and surfacing an error to the user.

In code, something along these lines:

onError: (e) => {
  console.error(e); // log it
  captureError(e); // send it to our external error monitoring provider
  setGlobalError(e); // surface it to the user; on internal tools this really might just be `window.alert` instead
}

It is extremely rare that we want to do anything more than the above.

alessbell commented 11 months ago

@jxdp for global error logging, have you considered using an Error link? It's well suited for this use case: it receives both network errors and GraphQL errors as the request travels back "up" the link chain.

Fwiw, you could also call window.alert from inside the error link if that's satisfactory for an internal tool :) I do see the value of creating a global error handling mechanism within the React render cycle, though I'm curious about a global onCompleted - is there a specific use case there?

I don't have an answer for when we'll be able to prioritize these global callbacks, but the above may be useful in shipping at least partial support for what you're looking for in the meantime.

ijxy commented 9 months ago

@alessbell

Using the Error link for logging seems like a great idea, thanks for that. We utilise an Error link for a certain classes of errors (eg errors caused by an API version mismatch) but I wouldn't want to do anything like window.alert here because it's not possible to change this behaviour dynamically (or at least much more complex).

Being able to set a default implementation for onCompleted would allow for things like having successful mutations automatically trigger a toast message, or updating react state (eg a global counter for how many operations the user has performed). There are a couple of ways to get something close to this behaviour, but they're much more work than just setting a default implementation in the ApolloClient constructor.

jerelmiller commented 9 months ago

@jxdp I'm curious if a custom ApolloLink would work for your use case. You might find it more reliable than onCompleted anyways since its a guaranteed network request. onCompleted is a bit spicy these days since there are many different ideas on the "right" behavior for that callback.

Would something like this work for you?

const successLink = new ApolloLink((operation, forward) => {
  return forward(operation).subscribe({
    next: (result) => {
      if (!result.errors) {
        incrementSuccessCount()
        Toast.success('You did it!')
        // etc.
      }
    }
  })
})

If you need to make it a bit more robust because you might be working with @defer or something that returns a multipart response and don't want to trigger the logic until the request is complete, this might be a bit better.

const successLink = new ApolloLink((operation, forward) => {
  let lastResult;

  return forward(operation).subscribe({
    next: (result) => {
      lastResult = result;
    },
    complete: () => {
      if (!lastResult.errors) {
        incrementSuccessCount()
        Toast.success('You did it!')
        // etc.
      }
    }
  })
})
ijxy commented 9 months ago

@jerelmiller

In principal something like that could could work, but I can see it quickly becoming a mass of if/else clauses in order to handle the different operation types and provide escape hatches for specific operations (eg we have some mutations fire off in the background that we don't want to handle in the same manner as when a user actively does something).

IMO the main criteria for this feature are:

  1. be able to specify the default behaviour for all success/error for queries/mutations
  2. be able to override any of the default behaviour for a specific operation

I think the existing onError and onCompleted callbacks are a compelling choice because they are existing APIs that have basically the right semantics (notwithstanding any recent spiciness, which I don't know the details of). What is missing is the ability to specify the default behaviour of those callbacks (or rather, to change the default behaviour, which is currently to do nothing).

You are right that throwing defer into the mix raises some additional questions, but if this feature builds on top of onError and onCompleted then no new ideas need to be introduced--it will just work however those callbacks work.

jerelmiller commented 9 months ago

@jxdp I'm realizing I think I misunderstood some of your initial requests. I see your initial description mentions mutations, which are much less spicy and controversial.

The spiciness I refer to is for the onCompleted callback on useQuery. We fixed an issue in v3.8.0 that stop onCompleted from being called for cache updates. Some devs relied on this this behavior in their apps, so it broke when upgrading. Essentially though we have 2 groups of devs that think the < 3.7 behavior is correct, and others that think the > 3.8 behavior is correct.

I could definitely see an argument for wanting a default callback on mutations, again because these are much less controversial and are much more straightforward. Its the query callbacks that we are much less hesitant to want to add support for. To be totally transparent, we are weighing the future of those query callbacks in future majors as we've seen them misused and misunderstood quite a few times.

Anyways, appreciate the context here! Just wanted to at least give an alternate idea to see what you're looking for and get a better idea of the types of things you're using it for. Thanks!

ijxy commented 9 months ago

@jerelmiller That is partially my fault, I did start talking more generically about "operations" in later posts. But yes, being able to set defaults for mutations would be a fantastic way to introduce this and, at least in our case, would provide a lot of value. Query support would be nice, but much less impactful.

Aha, I did not know about this debate. I definitely fall into the >3.8 crowd in that case--if there is no network request, there is nothing to "complete". 🤷‍♂️ Surprised to hear that it is highly contentious!