Search code examples
react-reduxredux-toolkitrtk-query

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


Solution

  • 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]);