query icon indicating copy to clipboard operation
query copied to clipboard

Tanstack Query for Svelte 5 does not rerender when adding a dependency variable

Open Boniqx opened this issue 10 months ago • 18 comments

Describe the bug

Issue Description:

Problem: When utilizing the createInfiniteQuery client to manage infinite queries, there is an issue with updating the query despite the addition of a dependency variable in the query key.

Steps to Reproduce:

Initialize a query using createInfiniteQuery. Define a queryKey that includes generic variables. Implement the queryFn function to handle data retrieval based on certain conditions. Attempt to update the query based on changes in the dependency variable within the queryKey.

Actual Behavior: Despite adding the dependency variable to the queryKey, the query does not update as expected when the variable changes.

Code Snippet:

javascript

let query = createInfiniteQuery({
  queryKey: ['Resource', dependencyVariable],
  queryFn: async ({ pageParam }) => {
    // Implementation omitted for brevity
  },
  getNextPageParam(lastPage) {
    // Implementation omitted for brevity
  },
  initialPageParam: null as string | null | undefined,
  initialDataUpdatedAt: () => Date.now(),
});

Environment:

Framework/Libraries: SVELTE 5 Possible Solutions:

Review the implementation of createInfiniteQuery to ensure proper handling of dependency variables. Check for any inconsistencies or errors in the query key setup. Consider alternative approaches for managing infinite queries that may better accommodate dynamic updates.

Your minimal, reproducible example

I have added an example code below

Steps to reproduce

Steps to Reproduce:

Initialize a query using createInfiniteQuery. Define a queryKey that includes generic variables. Implement the queryFn function to handle data retrieval based on certain conditions. Attempt to update the query based on changes in the dependency variable within the queryKey. Expected Behavior: The query should update accordingly when there are changes in the dependency variable included in the queryKey.

Expected behavior

Expected Behavior: The query should update accordingly when there are changes in the dependency variable included in the queryKey.

How often does this bug happen?

Every time

Screenshots or Videos

test

Platform

Any platform

Tanstack Query adapter

None

TanStack Query version

@tanstack/svelte-query

TypeScript version

5.4.3

Additional context

Additional Context: This issue impacts the ability to dynamically update queries based on changing dependency variables, hindering the functionality of the application.

Boniqx avatar Apr 05 '24 06:04 Boniqx

I have the same issue, I would expect infinite query to refetch page 0 with different parameters (or do anything at all) when part of the array key changes, as a temporary workaround I am using:

enabled: false

and manually triggering refetch in a reactive statement

$postQuery.refetch({ refetchPage: 0 });

halafi avatar Apr 08 '24 08:04 halafi

I have added an example code below

Please show a minimal reproduction with codesandbox or stackblitz

TkDodo avatar Apr 08 '24 09:04 TkDodo

To my knowledge, Tanstack Query has yet to support Svelte 5 runes, so directly feeding $state (assuming dependencyVariable is $state) won't yield the desired results.

The Tanstack Query 5 documentation mentions you can pass options as a Svelte Store to make it reactive. While not very ergonomic, it is workable.

Alternatives like Svelte 4 reactive statements or re-creating the query in a $effect won't work because svelte's context becomes unavailable after the component initialization is complete. (this accidentally did work in early versions of svelte 5)

To make it a bit more ergnomic you can simplify the creation of a reactive store from your runes with a function like:

const toReadable = (cb) => readable(cb(), set => $effect.pre(() => set(cb())));

and then do something like:

let dependency = $state("something");
const query = createQuery(toReadable(() => ({
  queryKey: ["Resource", dependency],
  queryFn: () => getSomething(dependency)
})));

However, note that the above example is for createQuery. For createInfiniteQuery, it appears to be a bit trickier. You might expect something like this to work:

let dependencyVariable = writable("whatever");
let query = createInfiniteQuery(
  derived(dependencyVariable, ($dependencyVariable) => ({
    queryKey: ["Resource", $dependencyVariable],
    queryFn: async ({ pageParam }) => {
      // Implementation omitted for brevity
    },
    getNextPageParam(lastPage) {
      // Implementation omitted for brevity
    },
    initialPageParam: null as string | null | undefined,
    initialDataUpdatedAt: () => Date.now(),
  }))
);

However, this gives me a lot of type errors, suggesting it is not the way to do it.


An additional example in the documentation specifically demonstrating how to use createInfiniteQuery reactively would be very helpful.

nicksulkers avatar Apr 11 '24 20:04 nicksulkers

Can you please create a stackblitz reproduction?

frederikhors avatar May 01 '24 18:05 frederikhors

I'm using Svelte 5 and needed to derive one of the variables in my createQuery. To achieve this, I tried wrapping createQuery in $derived. However, I encountered an error because createQuery uses getContext internally, and getContext can only be called on mount. Wrapping each use of the component with a derived query in {#key} was not a desirable solution.

The example provided by @nicksulkers wasn't working for me, as it resulted in the error: "$effect() can only be used as an expression statement."

To solve this, I created an abstraction using a readable and queryClient.ensureQueryData. This approach functions similarly but doesn't require a context, so it avoids breaking in Svelte 5's runes and re-rendering. The downside is that I no longer have state data, just the data I want or null. Given my limited coding time outside of work, this was the best solution I could come up with.

Here's the implementation:

import queryClient from "$lib/utilities/tanstack/queryClient";
import type { DefaultError, EnsureQueryDataOptions, QueryKey } from "@tanstack/svelte-query";
import { readable, type Readable } from "svelte/store";

export const ensureQuery = <TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>): Readable<TData | null> => {
  return readable<TData | null>(null, (set) => {
    queryClient.ensureQueryData(options).then(set);
  });
};

I then use it like this:

const ensuredQuery = $derived(
  ensureQuery({
    queryKey: ["Resource", dependency],
    queryFn: () => getSomething(dependency),
  })
);

I wish I know of a way to just do this:

const ensuredQuery = $ensureQuery({
  queryKey: ["Resource", dependency],
  queryFn: () => getSomething(dependency),
});

SaintPepsi avatar May 21 '24 11:05 SaintPepsi

@SaintPepsi here's the function as I'm actually using it:

utils.svelte.ts

export const reactiveQueryArgs = <T>(cb: () => T) => {
	const store = writable<T>();
	$effect.pre(() => {
		store.set(cb());
	});
	return store;
};

Usage could then be something like:

+page.svelte

let {search}: {search: string} = $props();

const query = createQuery(reactiveQueryArgs(() => ({
	queryKey: ["users", `search:${search}`],
	queryFn: () => fetch(`/users?search=${encodeURIComponent(search)}`)
})));

nicksulkers avatar May 21 '24 12:05 nicksulkers

@nicksulkers is reactiveQueryArgs in a .svelte.ts file?

SaintPepsi avatar May 21 '24 21:05 SaintPepsi

I was just reading through the source code and realised I can also just pass queryClient as second parameter to createQuery

SaintPepsi avatar May 21 '24 21:05 SaintPepsi

I was just reading through the source code and realised I can also just pass queryClient as second parameter to createQuery

What do you mean?

frederikhors avatar May 30 '24 19:05 frederikhors

I was just reading through the source code and realised I can also just pass queryClient as second parameter to createQuery

What do you mean?

For example I have my query client defined in $lib/utilities/tanstack/queryClient

You can pass your queryClient to your createQuery:

const query = createQuery({
        ...your query options,
    },
    queryClient <<<
)

Check the type of createQuery 😊

SaintPepsi avatar Jun 01 '24 11:06 SaintPepsi

Hopefully fixed by #6981

lachlancollins avatar Jul 18 '24 01:07 lachlancollins

@nicksulkers I'm using your amazing trick for quite some time now.

Today I got brutal error in my app:

Cannot read properties of undefined (reading 'teamId')

The code is this:

import { readable } from "svelte/store";

export const reactiveQueryArgs = <T>(cb: () => T) => {
  return readable(cb(), (set) => {
    $effect.pre(() => {
      set(cb());
    });
  });
};

const playerStore = createQuery(
  reactiveQueryArgs(() => ({
    queryKey: ["player", player_id],
    queryFn: getPlayer(player_id),
  }))
);

let { data, error, isLoading } = $derived($playerStore);

const teamStore = createQuery(
  reactiveQueryArgs(() => ({
    queryKey: ["team", data!.teamId],
    queryFn: getTeam(data!.teamId),
    enabled: !!data?.teamId,
  }))
);

I'm using data! because of the typescript error:

'data' is possibly 'null' or 'undefined'. ts(18049)

What do you think?

frederikhors avatar Aug 08 '24 11:08 frederikhors

@frederikhors what does !. do? MDN doesn't show any information on it: https://developer.mozilla.org/en-US/search?q=%21.

SaintPepsi avatar Aug 09 '24 23:08 SaintPepsi

@frederikhors what does !. do? MDN doesn't show any information on it: https://developer.mozilla.org/en-US/search?q=%21.

Is to shut-up typescript, telling it that I know data is there because I'm using enabled: !!data?.teamId.

frederikhors avatar Aug 09 '24 23:08 frederikhors

Oh true! 🤦

Technically speaking, even if data is null or undefined and you're checking that in enabled the value itself can still be null or undefined, which is why typescript complains.

const data = null

queryKey: ["team", data!.teamId],
queryFn: getTeam(data!.teamId),
enabled: !!data?.teamId,

Is equivelent to:

const data = null

queryKey: ["team", null.teamId],
queryFn: getTeam(null.teamId),
enabled: false,

so you'd probably have to do something like this:

queryKey: ["team", data?.teamId],
queryFn: getTeam(data?.teamId),
enabled: !!data?.teamId,

SaintPepsi avatar Aug 09 '24 23:08 SaintPepsi

@frederikhors Your error is likely due to

queryFn: getTeam(data!.teamId),

where getTeam (and specifically it's parameter) is executed immediately, before data contains a meaningful value.

You probably intended to do something like this:

queryFn: () => getTeam(data!.teamId),

where getTeam only gets called after the queryFn is triggered and we are certain data is valid because of the "enabled" property.

You'll probably want to do the same thing for your "getPlayer".

nicksulkers avatar Aug 10 '24 11:08 nicksulkers

@nicksulkers, If I use this code:

const teamStore = createQuery(
  reactiveQueryArgs(() => ({
    queryKey: ["team", data!.teamId],
    queryFn: () => getTeam(data!.teamId),
    enabled: !!data?.teamId,
  }))
);

let { data: teamData } = $derived($teamStore);

let { players } = $derived(teamData);

I get this error:

Property 'players' does not exist on type '(() => Promise<{ __typename?: "Team" | undefined; updatedAt?: Date | null | undefined; ... 21 more ...; }) | und...'. ts(2339)

frederikhors avatar Aug 24 '24 14:08 frederikhors

@frederikhors Please reference my comment to why that might be happening

Destructuring from undefined doesn't work, Imagine if your query hasn't run yet. that means teamData in let { data: teamData } will be undefined.

imagine it like this:

let { players } = $derived(undefined);

You'd probably use a Nullish coalescing operator in that line

let { players } = $derived(teamData ?? {});

Oh true! 🤦

Technically speaking, even if data is null or undefined and you're checking that in enabled the value itself can still be null or undefined, which is why typescript complains.

const data = null

queryKey: ["team", data!.teamId],
queryFn: getTeam(data!.teamId),
enabled: !!data?.teamId,

Is equivelent to:

const data = null

queryKey: ["team", null.teamId],
queryFn: getTeam(null.teamId),
enabled: false,

so you'd probably have to do something like this:

queryKey: ["team", data?.teamId],
queryFn: getTeam(data?.teamId),
enabled: !!data?.teamId,

SaintPepsi avatar Aug 24 '24 22:08 SaintPepsi