Skip to content

refetch vs invalidateQuery: inconsistent behavior of queryFn #5894

@avasuro

Description

@avasuro

Describe the bug

if queryFn uses some external variables, it's behavior is not the same when called using query.refetch and using queryClient.invalidateQueries:

  • query.refetch always uses latest version of queryFn
  • queryClient.invalidateQueries always uses some outdated version of queryFn: either the one that was first provided to useQuery or queryFn that was used in last query.refetch.

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/interesting-banzai-r3rfts

Steps to reproduce

  • Click on Change external param button - it will generate new value of externalParam variable ('1')
  • Click on Invalidate button - pay attention that param value in alert modal ('0') is not the same as current value of externalParam variable ('1')
  • Click on Refetch button - now externalParam in alert is up to date ('1')
  • Click on Invalidate button - new externalParam in alert is also up to date ('1')

Expected behavior

Behavior of queryFn should be consistent. So either:

  • Only first version of queryFn that was passed to useQuery should be used in both query.refetch and queryClient.invalidateQueries (so, it's up to the end user to care about using references to lateset versions of external variables inside queryFn)
    OR
  • both query.refetch and queryClient.invalidateQueries should use latest version of queryFn.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • OS: any
  • Browser: any
  • Version: any

Tanstack Query adapter

react-query

TanStack Query version

4.33.0

TypeScript version

No response

Additional context

In real life app this bug affects application a lot. In every queryFn in the app we use auth token approximately like this:

const token = useAuthToken();

useQuery({
  queryKey: [...],
  queryFn: async () => {
    return await fetch('/api/...', {
        headers: {
            'Authorization': `Bearer ${token}`
        }
    });
  }
});

So, token is retrieved from external hook, and then used inside queryFn.
Token expires after one hour. To prolong session we fetch new token in the background. So, every hour this external token variable changes. But if later we use queryClient.invalidateQueries() to invalidate this query - some old token is used to refetch the data because of this bug. And obviously that always leads to "token expired" error from backend.

For now we could solve that by storing reference to queryFn (using custom useStaticCallback helper):

const useStaticCallback = (fn) => {
    const ref = useRef(fn)
    ref.current = fn
    return useCallback(
        (...args) => ref.current.apply(undefined, args),
        []
    )
}

const token = useAuthToken();

useQuery({
  queryKey: [...],
  queryFn: useStaticCallback(async () => {
    return await fetch('/api/...', {
        headers: {
            'Authorization': `Bearer ${token}`
        }
    });
  })
});

UPD.: workaround above wouldn't work as expected in all possible cases. This proposal is the only proper way to go.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions