Manthan Jamdagni
Manthan Jamdagni

Reputation: 1031

Use RTK Query to poll API until expected data is received with a custom baseQuery

Hi I am new to RTK Query and Redux toolkit. I am working on a pet project where I am trying to poll a workflow status API until it gives me a workflow status as 'CLOSED' Now I am not sure how to poll this API within my component since I would have done that within a useEffect but react doesn't allow hooks within hooks so I can not use my useAPIQuery RTK-Query hooks within useEffect.

example apiSlice.ts -

const getBaseQuery = async (apiPromise: Promise<ServiceModel>) => {
  try {
    const response = await apiPromise;
    if (response) {
      return {
        data: response,
      };
    }
    throw new Error('No data received from service');
  } catch (err) {
    return {
      error: err,
    };
  }
};

export const api = createApi({
  reducerPath: 'api',
  baseQuery: getBaseQuery,
  endpoints: (builder) => ({
    runWorfklow: builder.query({
      query: ({ buildingId, deviceIds }) => getRunWorkflowPromise({ buildingId, deviceIds }),
      transformResponse: (response: RunWorkflowOutput) => ({ ...response }),
    }),
    getWorkflowStatus: builder.query({
      query: ({ buildingId, runId }) => getWorkflowStatusPromise({ buildingId, runId }),
      transformResponse: (response: GetWorkflowStatusOutput) => ({ ...response }),
    }),
  }),
});

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useRunWorfklowQuery, useGetWorkflowStatusQuery } = api;

example component -


const LoaderBox = (props: LoaderBoxProps) => {
  const { buildingId, devicesId } = props;

  const {
    data: runWorkflowData,
    error: runWorkflowError,
    isLoading: isRunWorkflowLoading,
    isSuccess: isRunWorkflowSuccess,
  } = useRunWorfklowQuery({
    buildingId,
    devicesId,
  });

  // This call works perfectly fine and give me runStatusData.runStatus as 'OPEN', showing it here for demonstrating how the API would work.
  const {
    data: runStatusData,
    error: runStatusError,
    isLoading: runStatusLoading,
    isUninitialized: runStatusUninitialized,
  } = useGetWorkflowStatusQuery(
    isRunWorkflowSuccess ? { buildingId, runId: runWorkflowData.runId } : skipToken,
  );

  const [isRunFinished, setIsRunFinished] = useState(false);

  useEffect(() => {
    // To continuously poll for WF status
    const poll = () => {
      const { data } = useGetWorkflowStatusQuery(
        isRunWorkflowSuccess ? { buildingId, runId: runWorkflowData.runId } : skipToken,
      );
      if (data?.runStatus === 'CLOSED') {
        setIsRunFinished(true);
      } else {
        setTimeout(() => {
          poll();
        }, 5000);
      }
    };
    poll();
  }, [isRunWorkflowSuccess, runWorkflowData, buildingId]);

return (
<>
 {!runWorkflowError ? (
  <>
   <Text> Requested workflow run</Text>
   isRunFinished ? <Text>Workflow finished</Text> : <Text>Workflow in progress</Text>
  </>
  )
  : <></>}
 {}
</>
);

Now I can make it work by not using the useGetWorkflowStatus hook and directly using the getWorkflowStatusPromise in my useEffect though I wanted to see if I can still use the RTK query hooks to get this done.

The other thing I tried was to add retry on the endpoints in my apiSlice, but there I am not able to do that because my baseQuery function doesn't accept any retry and if I try to add a retry param to my baseQuery function, createAPI doesn't allow that.

The other thing I tried was to use lazyQuery for initiating the GetWorfklowStatus calls after the runWorkflow call gets complete but that did not work either, it fails with an error - This expression is not callable. Type '[LazyQueryTrigger<QueryDefinition<any, (apiPromise: Promise<ServiceModel>) => Promise<{ data: ServiceModel; error?: undefined; } | { error: unknown; data?: undefined; }>, never, { ...; }, "api">>, UseQueryStateDefaultResult<...>, UseLazyQueryLastPromiseInfo<...>]' has no call signatures.

I'll appreciate any pointers which redirect me into the right direction and happy to provide more information on this if needed.

Thanks

Upvotes: 5

Views: 5425

Answers (1)

Drew Reese
Drew Reese

Reputation: 203208

I think you are close with the skipToken approach. The query hooks already have a polling capability built-in. I'd suggest something close to the following:

const runStatusDataRef = React.useRef();

const {
  data: runStatusData,
  error: runStatusError,
  isLoading: runStatusLoading,
  isUninitialized: runStatusUninitialized,
} = useGetWorkflowStatusQuery(
  { buildingId, runId: runWorkflowData.runId },
  {
    pollingInterval: 5000,
    skip: !isRunWorkflowSuccess || runStatusDataRef.current?.runStatus === 'CLOSED',
  }
);

React.useEffect(() => {
  runStatusDataRef.current = runStatusData;
}, [runStatusData]);

If you need to use skipToken you might get by with just setting the polling interval and passing the additional condition to the ternary expression:

const runStatusDataRef = React.useRef();

const {
  data: runStatusData,
  error: runStatusError,
  isLoading: runStatusLoading,
  isUninitialized: runStatusUninitialized,
} = useGetWorkflowStatusQuery(
  isRunWorkflowSuccess && runStatusDataRef.current?.runStatus !== 'CLOSED'
    ? { buildingId, runId: runWorkflowData.runId }
    : skipToken,
  { pollingInterval: 5000 },
);

React.useEffect(() => {
  runStatusDataRef.current = runStatusData;
}, [runStatusData]);

Upvotes: 2

Related Questions