0

I'm migrating a legacy codebase from saga (no cache) to react-query. My starting point is to replicate exactly as we have now, a very unoptimised approach, and optimise later, enabling cache piece by piece. I have the following immediate requirements:

  • I have no interest in loading stale data in the background.
  • I do not want cache by default
  • Every useQuery mount should refetch the data, loading as usual
  • I can enable the cache for individual queries

This is my query client:

client = new QueryClient({
  defaultOptions: { queries: { retry: false, staleTime: Infinity, cacheTime: 0 } },
});

I've written a wrapper around this which mimics our old API with the query fn, for migration sake, which looks like this:

export const useRequest = (
  url,
  opts,
) => {
  return result = useQuery({
    queryKey: [url],
    cacheTime: 0,
    ...opts,
    queryFn: () => request(url, 'GET'),
  });
};

I've written unit tests around this to ensure cache is disabled, which are:

    const { result: hook1, waitFor } = renderHook(() => useRequest('/jobs'), {
      wrapper,
    });

    await waitFor(() => expect(hook1.current.isSuccess).toBe(true));

    // Hook 2 load
    const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
      wrapper,
    });

    await waitFor(() => expect(hook2.current.isFetching).toBe(false));

    // Assert cache
    await waitFor(() => expect(handler).toBeCalledTimes(2));

Handler being a spy function around my API test harness.

Unfortunately, this is failing, and on debugging, it is loading data from the cache.

With a cache time of 0, and a stale time of Infinity, why is this loading data from the cache? I was under the impression a cache time of 0 would always invalidate the cache immediately.

I can fix this by removing staleTime: Infinity. However, this fails my last requirement.

Consider this second test, which ensures that if I enable the cache, my API does not get hit twice.

// Hook 1 load
    const { result: hook1, waitFor } = renderHook(() => useRequest('/jobs', { cacheTime: 1000 }), {
      wrapper,
    });

    await waitFor(() => expect(hook1.current.isSuccess).toBe(true));
    // Hook 2 load
    const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
      wrapper,
    });

    // Stale time will refetch in background
    await waitFor(() => expect(hook2.current.isFetching).toBe(false));

    // Assert cache
    await waitFor(() => {
      expect(handler).toBeCalledTimes(1);
    });

This fails if I remove staleTime, as naturally, the data will be stale and will refetch in the background.

My understanding is, if cacheTime is 0, then staleTime should not matter, as cache gets cleared out immediately. I've read all the docs I can to understand this, but I cannot seem to figure out why it behaves this way.

Could someone explain why the first test fails and loads from the cache, when cacheTime is 0?

plusheen
  • 1,128
  • 1
  • 8
  • 23
  • What you're trying to do doesn't make sense. Why not just set a sensible default for your cache time. I'm not sure how the mental model for the subscription based approach works if you try to circumvent your caching. – Chad S. Jul 12 '23 at 18:57
  • We're migrating a large legacy application to a new http mechanism, so we don't want to throw everything in at once. While I understand it seems odd to go for a SWR pattern and then not do any caching, it's just a migration step. It's also a good chance to really learn how the caching/observers work as I'm pretty fresh with the lib. I believe my issues stem from the fact `cacheTime` isn't really cache time, `staleTime` is. Though I'm not sure. – plusheen Jul 13 '23 at 07:47
  • I think you would have an easier time if you re-worded your question. I'm having a hard time understanding what you're actually trying to accomplish. Go though the use cases: `ComponentA requests QueryA` `ComponentB requests QueryA` `Component C returns
    ` Are you expecting one API call or two? What if Component C mounts component A and then onclick replaces it with component B. Do you expect an API call to occur?
    – Chad S. Jul 13 '23 at 20:22

1 Answers1

0

I believe the issue comes from the fact that the cache is not cleared for active queries, meaning that as long as useQuery is mounted cacheTime: 0 won't clear the cache for that query key.

You could play around with queryClient.invalidateQueries and invalidate the cache manually in useRequest, but since React is built around components and hooks being re-rendered frequently we need some kind of cache to store the API response from one render to the next, and I wouldn't expect this approach to be successful.

Something that might work for you is using unique query keys for the same request. This way we would have each instantiation of useQuery keeping a separate cache record instead of the cache being shared between all requests with the same url.

You could use a simple hook (useUniqueUrl) that adds a search parameter to the url to make it unique. Then later when you want instantiations to share the same cache you can simply remove the usage of useUniqueUrl.

function useUniqueUrl(url) {
  // generate a random value that is stable across re-renders
  const value = useMemo(() => Math.ceil(Math.random() * 100000), [])
  const u = new URL(url)
  u.searchParams.append("cacheUniqueness", value)
  return u.toString()
}

// example usage
function useAPI() {
   const url = getUrl()
   const uniqueUrl = useUniqueUrl(url)
   return useRequest(uniqueUrl)
}
Anton
  • 1,045
  • 1
  • 7
  • 16