query icon indicating copy to clipboard operation
query copied to clipboard

.refetch() is not working as expected after signal update when using injectQuery

Open tomer953 opened this issue 8 months ago • 2 comments

Describe the bug

It seems like injectInfiniteQuery that depends on some signal-based param, like query() for the queryKey/queryFn, is not working as expected when we use the .refetch() directly after updating the query signal, something like:

Edit: seems like relevant for injectQuery as well

  query = signal('');

  postsQuery = injectInfiniteQuery(() => ({
    queryKey: ['posts', this.query()],
    queryFn: ({ pageParam = 0 }) =>
      firstValueFrom(this.postsService.getPosts(pageParam, this.query())),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextId,
    getPreviousPageParam: (firstPage) => firstPage.previousId,
    enabled: false,
    placeholderData: keepPreviousData,
  }));

then

search() {
    this.query.set('foo')
    this.postsQuery.refetch()
}

This isn't working (view stackblitz)

The solution is to delay the refetch call:

search() {
    this.query.set('foo')
    setTimeout(() => this.postsQuery.refetch())
}

Your minimal, reproducible example

https://stackblitz.com/edit/sb1-6pedarrr?file=src%2Fapp%2Fcomponents%2Fposts-list%2Fposts-list.component.ts

Steps to reproduce

  1. type sit in the search input

  2. press Search button

  3. this will trigger a .refetch() with the updated search term and show the results as expected

  4. remove the setTimeout() in the end of the file, and call refetch() directly

    this.postsQuery.refetch();
    //setTimeout(() => this.postsQuery.refetch());
  1. this is no longer works as expected

Expected behavior

refetch() should work as expected without setTimeout()

How often does this bug happen?

None

Screenshots or Videos

No response

Platform

  • OS: macOS 15.5
  • Browser: latest chrome

Tanstack Query adapter

angular-query

TanStack Query version

v5.76.0

TypeScript version

v5.8.2

Additional context

in my environment with v5.51.15, the HTTP call is made with the wrong param (the previous query() value) and I tried to repro it with stackblitz, where it behaves differently (probably because of different versions) - but still not as expected.

tomer953 avatar May 22 '25 09:05 tomer953

That seems to be a behavior that is not specific to injectInfiniteQuery but would also be present for injectQuery.

ThiloAschebrock avatar May 23 '25 23:05 ThiloAschebrock

This likely a consequence of using effect in the definition of the injection function as effect unlike computed will only run eventually after the dependencies have been updated. As a fix, one could, therefore, consider removing the effects as proposed for injectMutation in https://github.com/TanStack/query/pull/9098.

ThiloAschebrock avatar May 24 '25 00:05 ThiloAschebrock

Hi :)

Any work regarding this issue? Its getting messy really quick once we go setTimeout the template has many potential glitches and invalid states caused by the internal use of effect

tomer953 avatar Jun 22 '25 17:06 tomer953

I think we found a workaround, fetching with the queryClient instead of directly use .refetch()

this.queryClient.fetchInfiniteQuery(options);

tomer953 avatar Jul 20 '25 14:07 tomer953

The problem might be not the query function itself but rather the object you return from builder. This approach works like wonder for me:

private readonly postsQuery = injectInfiniteQuery(() => {
  const postsFilter = this.query();
  return {
    queryKey: ['posts', postsFilter],
    queryFn: ({ pageParam = 0 }) =>
      firstValueFrom(this.postsService.getPosts(pageParam, postsFilter)),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextId,
    getPreviousPageParam: (firstPage) => firstPage.previousId,
    enabled: false,
    placeholderData: keepPreviousData,
  };
});

bebrasmell avatar Aug 31 '25 08:08 bebrasmell

@bebrasmell I don't see how this actually matters (reading the signal before or inside the return.

you can see here it does not fix the problem https://stackblitz.com/edit/sb1-t6c4vtyt?file=src%2Fapp%2Fcomponents%2Fposts-list%2Fposts-list.component.ts

its a timing issue

tomer953 avatar Aug 31 '25 13:08 tomer953

looking at the initial reproduction:

search() {
    this.query.set('foo')
    this.postsQuery.refetch()
}

since query is (correctly) part of the key:

queryKey: ['posts', this.query()],

there is no need to manually call refetch. Changing a value that is part of the key will result in the QueryObserver starting to observe a different Query, and if there’s no data or stale data for that Query, you’ll get an automatic refetch.

The react equivalent would be:

  const [query, setQuery] = useState('')

  postsQuery = useInfiniteQuery({
    queryKey: ['posts', query],
    queryFn: ({ pageParam = 0 }) =>
      firstValueFrom(this.postsService.getPosts(pageParam, this.query())),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextId,
    getPreviousPageParam: (firstPage) => firstPage.previousId,
    enabled: false,
    placeholderData: keepPreviousData,
  });

then:

search() {
    setQuery('foo')

TkDodo avatar Sep 02 '25 08:09 TkDodo

there is no need to manually call refetch

But the query is enabled: false

So changing the key isn’t enough, therefore the refetch() call.

You can try the stackblitz Example and look on the network calls

tomer953 avatar Sep 02 '25 11:09 tomer953

but why is the query enabled: false ? If you want to disable the query while it’s empty, you can do:

enabled: query !== ''

This is documented under lazy queries:

https://tanstack.com/query/latest/docs/framework/angular/guides/disabling-queries#lazy-queries

As it stands, refetch will always execute the queryFn it has currently stored, which means it will close over all values and have access to the queryKey from creation time. This is expected.

If calling setQuery('foo') doesn’t synchronously re-run the component and thus update the Query, this is expected.

React works the same way: A call to setState merely schedules an update, so calling refetch() right after that won’t see the latest value, except if you call flushSync() in between.

Mostly, you don’t want enabled: false. We also document this:

Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch

TkDodo avatar Sep 02 '25 13:09 TkDodo