query
query copied to clipboard
Tanstack Query for Svelte 5 does not rerender when adding a dependency variable
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.
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 });
I have added an example code below
Please show a minimal reproduction with codesandbox or stackblitz
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.
Can you please create a stackblitz reproduction?
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 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 is reactiveQueryArgs
in a .svelte.ts
file?
I was just reading through the source code and realised I can also just pass queryClient
as second parameter to createQuery
I was just reading through the source code and realised I can also just pass
queryClient
as second parameter tocreateQuery
What do you mean?
I was just reading through the source code and realised I can also just pass
queryClient
as second parameter tocreateQuery
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
😊
Hopefully fixed by #6981
@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 what does !.
do?
MDN doesn't show any information on it: https://developer.mozilla.org/en-US/search?q=%21.
@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
.
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,
@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, 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 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
isnull
orundefined
and you're checking that inenabled
the value itself can still benull
orundefined
, 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,