1

I've implemented a custom React hook creates a task on my API, stores the task UUID in state and then starts polling the API every 2 seconds until successful data is received.

The problem I'm facing is that I want to display the last successful data returned from the API while it's fetching or refetching for new data. Right now the query result is undefined when mutate is called. I don't want that.

The API is slow right now and it takes about 12 seconds or 6 refetches to complete the request successfully, so I want to display the old data meanwhile. I think you understand what I'm trying to say by now.

You can ignore the sendToBackground(), it's how you communicate with background service workers using Plasmo.

Tools I'm using:

  • Plasmo 0.81.0
  • React 18.2.0
  • React Query ^3.39.3

use-suggestions.ts

export function useSuggestions(
  text: string,
  id: string,
): UseQueryResult<TaskTextStatus> {
  const [task, setTask] = useState<string>();
  const [stopRefetch, setStopRefetch] = useState(false);
  const [textDebounced] = useDebounce(text, 1000);

  // Triggers on input text change, calls mutate once per 1000ms
  useEffect(() => {
    mutate(text);
  }, [textDebounced]);

  // Submits text task to API, stores task UUID in `task` state
  const { mutate } = useMutation(
    ["suggestions", id, text],
    async (text: string) => {
      if (!text) return;
      const res = await sendToBackground<{ text: string }, TaskTextSubmit>({
        name: "send-text",
        body: { text },
      });
      return res;
    },
    {
      onSuccess(data) {
        if (!data) return;

        // Sets the created task UUID
        setTask(data.task_uuid);
      },
      onError(e) {
        console.log(e, "ERROR");
      },
    },
  );

  // Activates when task_uuid is known(when task is created)
  // Polls the API every 2 seconds in the background
  // Stops when receives result
  return useQuery(
    ["suggestions", id, text],
    async () => {
      if (!task) return;
      setStopRefetch(false);
      const res = await sendToBackground<{ task_uuid: string }, TaskTextStatus>(
        { name: "check-task", body: { task_uuid: task } },
      );

      if (res.is_success) return res;
    },
    {
      enabled: !!task,
      refetchInterval: stopRefetch ? false : 2000,
      keepPreviousData: true,
      refetchIntervalInBackground: true,
      refetchOnWindowFocus: false,
      onSuccess(data) {
        if (data?.is_success) {
          setStopRefetch(true);
          setTask("");
        }
      },
      onError() {
        setStopRefetch(true);
      },
    },
  );
}

With keepPreviousData addded, it returns the old data only after the first fetch. Then it sets to undefined because no data was returned from the following refetch.

Essentially, I need to keep the last data where is_success was true, up until receiving new data where is_success is also true, keeping out all the whatnot happens in the middle while refetching.

Hope I didn't make it too confusing, thanks for the help!

1
  • 2
    Adding a lot of extra complexity to support having an inconsistent or unreliable api is probably less efficient than fixing your api. Also I'd consider calling a mutation function in a useEffect an anti-pattern which is further indication that you're working against your tools rather than with them. Commented Aug 4, 2023 at 17:51

3 Answers 3

6

In React Query version 5 or above, you can use placeholderData to keep the previous data during fetching new data,

useQuery({
    queryKey: ['employee-list'],
    queryFn: async () => {
        return await ...
    },
    placeholderData: (prev) => prev,
    staleTime: 300000, // 5 minutes
});
Sign up to request clarification or add additional context in comments.

Comments

3

Some feedback:

  • you should throw an error when the fetch is unsuccessful
  • data is not cleared just because the most recent fetch errored
  • keepPreviousData: true does not solve your problem, it means the data will be kept during fetching when the query key changes. So in your example, if id changes from 1 to 2, then data from 1 will be displayed to user while 2 is fetching. Consider if you really need it.
  • remove stopRefetch from state. If you want really want to disable a query then set enabled to false

Your updated queryFn can look like this:

async () => {
  const res = await sendToBackground<{ task_uuid: string }, TaskTextStatus>(
    { name: "check-task", body: { task_uuid: task } },
  );

  if (!res.is_success) { throw new Error('Fetch not successful') }

  return res;
},

Comments

2

This happened to me while implementing back pagination for a webapp and changing the queryKey yet still wanting to keep track of my total while fetching to not have undefined data

Using @tanstack/react-query v5, keepPreviousData has been replaced by placeholderData but you can still achieve the same behavior by using the following :

import { keepPreviousData, useQuery } from '@tanstack/react-query';
...
useQuery({
    queryKey: ['listeFiche', page, rowsPerPage, filters],
    queryFn: () => handleError(() => getListeFiches(start, end, filters), alert),
    placeholderData: keepPreviousData
});
...

Here is the documentation I have used : https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function

In my case, this was still not enough because my initialData would reset my query to 0.

What I did was to dynamically handle total state so my initialData would always have previous data, I can then stop using placeholderData: keepPreviousData

import { keepPreviousData, useQuery } from '@tanstack/react-query';
...
export function useGetUserFiches(name, page, rowsPerPage, defaultTotal = 0) {
    const start = page * rowsPerPage;
    const end = start + rowsPerPage;
    const alert = useAlert();
    return useQuery({
        queryKey: ['userFiches', name, page, rowsPerPage],
        queryFn: () => handleError(() => getUserFiches(name, start, end), alert),
        initialData: HTTPQueryResult([], defaultTotal),
        enabled: true,
        placeholderData: keepPreviousData
    });
}
...
const [totalValue, setTotalValue] = useState(0);
const { data, isError, isFetching, refetch } = useGetUserFiches(username, page, rowsPerPage, totalValue);
const { total, data: userFiches } = data;

useEffect(() => {
    setTotalValue(total);
}, [total]);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.