GNG
GNG

Reputation: 1531

Managing redux toolkit query loading state with polling

I have a REST API endpoint and a react app that uses redux toolkit (RTK). The UI displays a loading state when fetching the endpoint and refetches the endpoint every 15 seconds to poll for new data. The reason we poll is because some fields in the response are prone to change. However, the data that hydrates the parts of the UI that show a loading state does not change.

The loading state is currently true when the query hook's isFetching prop is true. This poses a problem since isFetching is true during the refetches as well, and we don't want the loading state to show during refetches. How should I leverage RTK best to manage this issue?

We want to show the loading state while initially fetching data and also whenever request body parameters change. For example, one parameter in the request body denotes the time window.

One idea is to use the isLoading parameter that the query hook provides rather than isFetching. However, I'm seeing that this prevents the loading state from showing up in the scenario where the UI refetches due to request parameter changes.

Here is a minimal reproducible example:

import { useGetFooDataQuery } from '../app/services/foo';
import { CircularProgress } from '@mui/material';
const POLLING_INTERVAL = 20000;
const FooComponent = ({ orgId }: { orgId: string }) => {
  const {
    data: foo,
    isLoading: isLoading,
    isFetching: isFetching,
  } = useGetFooDataQuery(
    {
      org_id: orgId,
    },
    {
      pollingInterval: POLLING_INTERVAL,
      refetchOnMountOrArgChange: true,
    },
  );
  const { data_that_only_changes_when_org_id_changes, polled_data_prone_to_change } = foo ?? {};

  return (
    <div>
      <div>{isFetching ? <CircularProgress /> : data_that_only_changes_when_org_id_changes}</div> // The spinner should appear only on the initial load and during fetches that are triggered because the org_id has changed.
      <div>{isFetching ? <CircularProgress /> : polled_data_prone_to_change}</div> // The spinner should appear during every refetch.
    </div>
  );
};

export default FooComponent;
import { api } from './api';

type GetFooDataQueryParams = {
  org_id: string;
};

type FooData = {
  data_that_only_changes_when_org_id_changes: number;
  polled_data_prone_to_change: number;
};

export const fooApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getFooData: builder.query<FooData, GetFooDataQueryParams>({
      query: (args) => {
        const body = {
          org_id: args.org_id,
        };
        return {
          url: 'data/foo',
          method: 'GET',
          body: body,
        };
      },
    }),
  }),
});

export const { useGetFooDataQuery } = fooApi;
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { RootState } from '../store'; 
import { getApiURL } from '../../utilities/url';

const baseQuery = fetchBaseQuery({
  baseUrl: getApiURL([]),
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }
    headers.set('Accept', 'application/json');
    headers.set('Content-Type', 'application/json');
    return headers;
  },
});


export const api = createApi({
  
  reducerPath: 'splitApi',
  baseQuery: baseQuery,
  endpoints: () => ({}),
});

export const enhancedApi = api.enhanceEndpoints({
  endpoints: () => ({
    getPost: () => 'test',
  }),
});

I have thought through this problem a bit more and returned to this post. Here is a solution that I think is serving the requirement. It works by keeping track of whether a query is triggered due to argument changes or an instance of polling. But perhaps there is a better solution.

import { useEffect, useState } from 'react';
import { useGetFooDataQuery } from '../services/fooService';
import { CircularProgress } from '@mui/material';
const POLLING_INTERVAL = 20000;

const FooComponent = ({ orgId }: { orgId: string }) => {
  const [mountOrQueryArgChange, setMountOrQueryArgChange] = useState<boolean>(false);

  const { data: foo, isFetching: isFetching } = useGetFooDataQuery(
    {
      org_id: orgId,
    },
    {
      pollingInterval: POLLING_INTERVAL,
      refetchOnMountOrArgChange: true,
    },
  );

  useEffect(() => {
    setMountOrQueryArgChange(true);
  }, [orgId]);

  useEffect(() => {
    if (!isFetching) setMountOrQueryArgChange(false);
  }, [isFetching]);

  const { data_that_only_changes_when_org_id_changes, polled_data_prone_to_change } = foo ?? {};

  return (
    <div>
      {/* The spinner should appear only on the initial load and during fetches that are triggered because the org_id has
      changed. */}
      <div>
        {mountOrQueryArgChange && isFetching ? <CircularProgress /> : data_that_only_changes_when_org_id_changes}
      </div>

      {/* The spinner should appear during every refetch. */}
      <div>{isFetching ? <CircularProgress /> : polled_data_prone_to_change}</div>
    </div>
  );
};

export default FooComponent;

Upvotes: 1

Views: 47

Answers (1)

Drew Reese
Drew Reese

Reputation: 203333

From the docs:

  • isLoading: Query is currently loading for the first time. No data yet.
  • isFetching: Query is currently fetching, but might have data from an earlier request.

This means isFetching is true any time the endpoint is actively fetching data, regardless of endpoint argument value.

You can use a FooData response property that appears to be coupled to the query value to conditionally render the loading indicator only when fetching new data, e.g. when the id of the current query doesn't match the id of the currently cached data. I'll assume the query org_id1 is also part of the returned data, to be used for this purpose.

1 Note: If org_id is not included in the response data you can use transformResponse to inject it for the UI usage, or use queryFn which allows direct manipulation of the response value.

Example:

type FooData = {
  org_id: string; // <-- foo data id
  data_that_only_changes_when_org_id_changes: number;
  polled_data_prone_to_change: number;
};
const FooComponent = ({ orgId }: { orgId: string }) => {
  ...

  const { data: foo, isFetching } = useGetFooDataQuery(
    { org_id: orgId },
    {
      pollingInterval: POLLING_INTERVAL,
      refetchOnMountOrArgChange: true,
    },
  );

  ...

  return (
    <div>
      ...

      {/* The spinner should appear only when fetching new data */}
      <div>
        {(isFetching && foo?.org_id !== orgId)
          ? <CircularProgress />
          : (
            <>
              {/* the data you want to display */}
            </>
          )
        }
      </div>

      ...
    </div>
  );
};

Upvotes: 0

Related Questions