Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polling & RefetchQuery + uncaught error #12231

Open
pieterjandebruyne opened this issue Dec 17, 2024 · 1 comment
Open

Polling & RefetchQuery + uncaught error #12231

pieterjandebruyne opened this issue Dec 17, 2024 · 1 comment

Comments

@pieterjandebruyne
Copy link

Issue Description

When refetchQueries return an error from backend, there is an uncaught error thrown from apollo client instead of passing it to the onError callback of said query.

Also it seems like there is not a god way to retrigger polling for a specific document query.

Are these polling correct flows, and would we expect an error thrown from the server to be uncaught ?

Link to Reproduction

https://codesandbox.io

Reproduction Steps

Start polling when components loads

const skipPolling = useRef(false);
  const { data } = useXXXByIdQuery({
    variables: {
      id: 'abc',
    },
    pollInterval: 500,
    skipPollAttempt: () => skipPolling.current,
    onCompleted: () => {
      const finalState = Math.random() < 0.2;
      if (finalState) {
        skipPolling.current = true;
      } else {
        skipPolling.current = false;
      }
    },
    notifyOnNetworkStatusChange: true,
  });

Make use of

pollInterval + skipPollAttempt (make sure to use useRef instead of useState or the skipPollAttempt won’t have the correct value to work with.

notifyOnNetworkStatusChange: true, if we do not set this flag, polling requests won’t trigger onCompleted

If we use refetchQueries: [XXXByIdDocument] and we want the request to then also start polling as before, we need to add an else check that would reset the polling again

else {
  skipPolling.current = false;
}

But what if this first refetch errors out? You would think maybe we can add skipPolling.current = false; in de onError callback, but in fact if the query triggered by refetchQueries errors, onError does not get hit and we get an uncaught error in ApolloClient (INVALID_RESPONSE_MESSAGE is the message of the error returned by the server)

image

If you need to call startPolling conditionally:

const DEFAULT_POLL_INTERVAL = 10000;
const { data, startPolling, stopPolling } = useXXXByIdQuery({
   variables: {
     id: 'abc',
   },
   onCompleted: () => {
     const finalState = <whatever logic>
     if (finalState) {
       stopPolling();
     } else {
       startPolling(DEFAULT_POLL_INTERVAL);
     }
   },
   onError: () => {
     startPolling(DEFAULT_POLL_INTERVAL);
   },
   notifyOnNetworkStatusChange: true,
 });
 // start wheneever you want
 startPolling(DEFAULT_POLL_INTERVAL);

Explanation:
1: startPolling, stopPolling Pull the 2 polling functions from the query.
2: use useEffect to make sure we start polling as soon as the component is loaded. (if we only add it on the onCompleted hook, we won’t poll if the first request errors.
3: optionally sometimes you want to stop polling once a certain state is used. We can use the stopPolling.

 if (finalState) {
.  stopPolling();
}

4: Sometimes we also want to refetch queries based on another query. Let’s say we poll until a status is either FINISHED or ERROR. but if it is ERROR, there might be another mutation you can call to retry the operation. this mutation might reset the status to RUNNING.
So what we want is to use refetchQueries: [XXXByIdDocument], By default this would just retrigger 1 extra fetch. But if we want it again to keep polling untill finalState is reached, we have to make sure we also call startPolling(DEFAULT_POLL_INTERVAL);

5: now the last important step is to add

notifyOnNetworkStatusChange: true,

without this flag as true, onCompleted callback does not get triggered after a polling request returns.

and because this first request could also fail, we want to add it in onError. (startPolling does not trigger multiple times, so no need to worry about multiple threats each polling on their own interval). But again, if it does error, onError is not triggered

onError: () => {
  startPolling(DEFAULT_POLL_INTERVAL);
},

if you want to do some testing here is a snippet used in my tests:

import { FC, memo, useRef } from 'react';
import {
  UserByIdDocument,
  useUserByIdQuery,
  useUpdateUserMutation,
} from '@/gql/generated_gql.ts';
import { Button } from './Button';
interface ITestPollingProperties {
  className?: string;
}
export const TestPolling: FC<ITestPollingProperties> = ({ className }) => {
  const skipPolling = useRef(false);
  const { data } = useUserByIdQuery({
    variables: {
      id: 'abc',
    },
    pollInterval: 500,
    skipPollAttempt: () => {
      console.log('should skip?', skipPolling.current);
      return skipPolling.current;
    },
    onCompleted: () => {
      console.log('ON COMPLETE');
      const finalState = Math.random() < 0.2;
      if (finalState) {
        skipPolling.current = true;
      } else {
        skipPolling.current = false;
      }
    },
    onError: () => {
      console.log('ON ERROR, set false');
      skipPolling.current = false;
    },
    notifyOnNetworkStatusChange: true,
  });
  const [updateUser, { loading: updateUserLoading }] =
    useUpdateUserMutation({
      refetchQueries: [UserByIdDocument],
    });
  return (
    <div className={className}>
      hasData: {JSON.stringify(data != null)}
      skipPolling: {skipPolling.current ? 'true' : 'false'}
      <br />
      <Button
        onClick={async () => {
          await updateApplication({
            variables: {
              updateApplicationData: {
                id: 'cm3ip8f3a1fb1ib3z6k45frw2',
                name: 'test 12',
              },
            },
          });
        }}>
        trigger other
      </Button>
    </div>
  );
};

### `@apollo/client` version

3.11.10
@phryneas
Copy link
Member

Hi @pieterjandebruyne!

This is a lot to take in, and at this point, I'm not sure yet if it is a bug report, a feature request, or a usage question (the issue tracker is not really the best place for those).

Could you maybe simplify this a bit and create a runnable reproduction of what you're experiencing?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants