query
query copied to clipboard
Tanstack query results are not always reactive in SolidJS as they sometimes return a non-proxy object
Describe the bug
In SolidJS / Solid-Start (current versions 1.8.5 / 0.3.10) and current tanstack (5.8.3) if I take the results of a tanstack query and put them into a provider they are not reactive. But the results of routeData Resource is reactive.
In the following code, all values update other than the nested component using the context provider for the tanstack query.
import {RouteDataFuncArgs, useNavigate, useParams, useRouteData, useSearchParams} from "@solidjs/router";
import {isServer} from "solid-js/web";
import {createQuery, QueryFunctionContext, useQueryClient} from "@tanstack/solid-query";
import {createContext, createEffect, onCleanup, onMount, Show, useContext} from "solid-js";
import server$, {createServerData$} from "solid-start/server";
type QueryResultType = { message: string };
const queryOneFunction = server$(async (ctx: QueryFunctionContext): Promise<QueryResultType> => {
console.debug("queryOne server call", ctx.queryKey);
return { message: `${ctx.queryKey}` };
}
);
const manualQuery = server$(async (id: number) => {
const queryKey = ['queryTwo', id];
console.debug("queryTwo server call", queryKey);
return { message: `${queryKey}` };
})
const dataContext = createContext<QueryResultType>();
export function routeData(args: RouteDataFuncArgs) {
console.log("routeData", isServer ? "on server" : "on client", "id=", args.location.query.id ?? 0);
if (isServer) {
const id = Number(args.location.query.id ?? 0);
if (id == 0) {
console.debug("redirect to start");
args.navigate('/reactivity?id=1');
return;
}
const queryClient = useQueryClient();
void queryClient.prefetchQuery({
queryKey: ['queryOne', id],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
}).then(() => {
console.debug("routeData", isServer ? "on server" : "on client", "PRELOAD COMPLETE")
})
}
return createServerData$(async ([id])=> {
const queryKey = ['queryTwo', id];
console.debug("queryTwo server call", queryKey);
return { message: `${queryKey}` };
}, { key: () => [Number(args.location.query.id ?? 0) ], deferStream: true });
}
export default function TestReactivity() {
const [searchParams, setSearchParams] = useSearchParams();
const id = () => Number(searchParams.id ?? 0);
const queryOne = createQuery(() => ({
queryKey: ['queryOne', id()],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
placeholderData: (d)=>d
}));
const queryTwo = useRouteData<typeof routeData>()!;
createEffect(() => {
console.debug("queryOne state", queryOne.isFetching, queryOne.isSuccess, queryOne.isError, JSON.stringify(queryOne.data));
});
createEffect(() => {
console.debug("queryTwo", queryTwo.loading, queryTwo.state, JSON.stringify(queryTwo()));
});
onMount(()=> {
const timer = setInterval(() => {
let nextId = id() + 1;
if (nextId === 11) {
nextId = 1;
}
setSearchParams({id: nextId});
}, 1000)
onCleanup(()=>clearInterval(timer));
});
return <><div>
<div>Test Reactivity</div>
<br/>
<Show when={queryOne.data}>
<div>tanStack in component: {queryOne.data?.message ?? ''}</div>
</Show>
<Show when={queryOne.data}>
<dataContext.Provider value={queryOne.data}>
<SubComponent />
</dataContext.Provider>
</Show>
<Show when={queryTwo()}>
<div>routeData in component: {queryTwo()!.message}</div>
</Show>
<Show when={queryTwo()}>
<dataContext.Provider value={queryTwo()}>
<SubComponent />
</dataContext.Provider>
</Show>
<br/>
<div>Test Reactivity</div>
</div></>
}
function SubComponent() {
const data = useContext(dataContext);
return <>
<div>Using provider: {data?.message}</div>
</>
}
Your minimal, reproducible example
see in description
Steps to reproduce
see test case in main description.
Expected behavior
The results are reactive via a provider.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
all /a nyh
Tanstack Query adapter
solid-query
TanStack Query version
5.8.3
TypeScript version
4.4.9
Additional context
No response
The problem is that the first time data is returned (in this case very quickly) it is NOT a proxy, but later it is. And the first run breaks the reactive graph.
updated code with the logging:
import {RouteDataFuncArgs, useRouteData, useSearchParams} from "@solidjs/router";
import {isServer} from "solid-js/web";
import {createQuery, QueryFunctionContext, useQueryClient} from "@tanstack/solid-query";
import {createContext, createEffect, onCleanup, onMount, Show, useContext} from "solid-js";
import server$, {createServerData$} from "solid-start/server";
type QueryResultType = { message: string };
const queryOneFunction = server$(async (ctx: QueryFunctionContext): Promise<QueryResultType> => {
console.debug("queryOne server call", ctx.queryKey);
return { message: `${ctx.queryKey}` };
}
);
const manualQuery = server$(async (id: number) => {
const queryKey = ['queryTwo', id];
console.debug("queryTwo server call", queryKey);
return { message: `${queryKey}` };
})
const dataContext = createContext<QueryResultType>();
export function routeData(args: RouteDataFuncArgs) {
console.log("routeData", isServer ? "on server" : "on client", "id=", args.location.query.id ?? 0);
if (isServer) {
const id = Number(args.location.query.id ?? 0);
if (id == 0) {
console.debug("redirect to start");
args.navigate('/reactivity?id=1');
return;
}
const queryClient = useQueryClient();
void queryClient.prefetchQuery({
queryKey: ['queryOne', id],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
}).then(() => {
console.debug("routeData", isServer ? "on server" : "on client", "PRELOAD COMPLETE")
})
}
return createServerData$(async ([id])=> {
const queryKey = ['queryTwo', id];
console.debug("queryTwo server call", queryKey);
return { message: `${queryKey}` };
}, { key: () => [Number(args.location.query.id ?? 0) ], deferStream: true });
}
export default function TestReactivity() {
const [searchParams, setSearchParams] = useSearchParams();
const id = () => Number(searchParams.id ?? 0);
const queryOne = createQuery(() => ({
queryKey: ['queryOne', id()],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
// placeholderData: (d)=>d
}));
const queryTwo = useRouteData<typeof routeData>()!;
createEffect(() => {
console.debug("queryOne state", queryOne.isFetching, queryOne.isSuccess, queryOne.isError, queryOne);
console.debug("queryOne data", queryOne.data, JSON.stringify(queryOne.data));
});
createEffect(() => {
console.debug("queryTwo state", queryTwo.loading, queryTwo.state, queryTwo(), JSON.stringify(queryTwo()));
});
onMount(()=> {
const timer = setInterval(() => {
let nextId = id() + 1;
if (nextId === 11) {
nextId = 1;
}
setSearchParams({id: nextId});
}, 1000)
onCleanup(()=>clearInterval(timer));
});
return <><div>
<div>Test Reactivity</div>
<br/>
<Show when={queryOne.data}>
<div>tanStack in component: {queryOne.data?.message ?? ''}</div>
</Show>
<dataContext.Provider value={queryOne.data}>
<SubComponent />
</dataContext.Provider>
<Show when={queryTwo()}>
<div>routeData in component: {queryTwo()!.message}</div>
</Show>
<dataContext.Provider value={queryTwo()}> {/* Resource<WhateverMyData> */}
<SubComponent />
</dataContext.Provider>
<br/>
<div>Test Reactivity</div>
</div></>
}
function SubComponent() {
const data = useContext(dataContext);
console.debug("IS IT A PROXY", data);
return <>
<div>Using provider: {data?.message}</div>
</>
}
This is solidjs not realted to solid-start problem.
@marbemac Shouldn't this be changed?
onHydrated(_k, info) {
const defaultOptions = defaultedOptions()
if (info.value) {
hydrate(client(), {
queries: [
{
queryKey: defaultOptions.queryKey,
queryHash: defaultOptions.queryHash,
state: info.value,
},
],
})
}
```
to wrap `info.value` in a new store so it is reactive?
appears broken in https://github.com/TanStack/query/pull/5775 when `unwrapped` on the server needs re-wrapped on the client.
to wrap
info.valuein a new store so it is reactive?
Hmm, possibly? If you make that change does it resolve your reproduction?
no, appears to happen too late. Ryan was thinking there could be a timing issue with changes he made to solid around that time.
is this issue fixed? I can see that #5775 got merged already ?
@TkDodo no, that was already present in the codebase before my report, and maybe is part of the cause, OR a change to solid at the same time that caused a timing issue.
Still broken, checked with latest 5.25.0 and with solid 1.8.7 and 1.8.15
will test with latest solid-start soon in case the issue is there in timing of hydration
@ardeora
Updated to latest SolidJS, SOlid-Start beta 0.6.x and still fails. code updated for latest below. It breaks badly and is not proxied on first hydration, and then goes back to pending state even when it shouldn't, etc.
import {
redirect,
RouteLoadFuncArgs,
useSearchParams,
} from "@solidjs/router";
import { isServer } from "solid-js/web";
import {
createQuery,
QueryFunctionContext,
useQueryClient,
} from "@tanstack/solid-query";
import {
createContext,
createEffect,
onCleanup,
onMount,
Show,
Suspense,
useContext,
} from "solid-js";
type QueryResultType = { message: string };
const queryOneServerFunction = async (
ctx: QueryFunctionContext,
): Promise<QueryResultType> => {
"use server";
console.debug("queryOne server call", ctx.queryKey);
return { message: `${ctx.queryKey}` };
};
const queryOneFunction = async (
ctx: QueryFunctionContext,
): Promise<QueryResultType> => {
console.debug("queryOne client call", ctx.queryKey);
return await queryOneServerFunction(ctx);
};
const dataContext = createContext<QueryResultType>();
const loadRouteData = ({ location }: RouteLoadFuncArgs) => {
const id = Number(location.query.id ?? 0);
console.log("routeData", isServer ? "on server" : "on client", "id=", id);
if (isServer) {
if (id == 0) {
console.debug("redirect to start");
return redirect("/reactivity?id=1");
}
const queryClient = useQueryClient();
console.debug("prefetch query");
void queryClient
.prefetchQuery({
queryKey: ["queryOne", id],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
})
.then(() => {
console.debug(
"routeData",
isServer ? "on server" : "on client",
"PRELOAD COMPLETE",
);
});
}
};
// autoload when route is loaded
export const route = {
load: (args: RouteLoadFuncArgs) => loadRouteData(args),
};
// actual page
export default function TestReactivity() {
const [searchParams, setSearchParams] = useSearchParams();
const id = () => Number(searchParams.id ?? 0);
const queryOne = createQuery(() => ({
queryKey: ["queryOne", id()],
queryFn: queryOneFunction,
staleTime: 20 * 1000,
placeholderData: (d) => d,
}));
createEffect(() => {
console.debug(
"queryOne state",
queryOne.status,
queryOne.isFetching,
queryOne.isSuccess,
queryOne.isError,
queryOne,
);
console.debug(
"queryOne data",
queryOne.data,
JSON.stringify(queryOne.data),
);
});
onMount(() => {
const timer = setInterval(() => {
let nextId = id() + 1;
if (nextId === 11) {
nextId = 1;
}
setSearchParams({ id: nextId });
}, 3000);
onCleanup(() => clearInterval(timer));
});
return (
<>
<div>
<div>Test Reactivity</div>
<br />
<Suspense>
<div>
<Show when={queryOne.data}>
<div>tanStack in component: {queryOne.data?.message ?? ""}</div>
</Show>
<dataContext.Provider value={queryOne.data}>
<SubComponent />
</dataContext.Provider>
</div>
</Suspense>
<br />
<div>Test Reactivity</div>
</div>
</>
);
}
function SubComponent() {
const data = useContext(dataContext);
console.debug("IS IT A PROXY", data);
return (
<>
<div>Using provider: {data?.message}</div>
</>
);
}
client logs:
routeData on client id= 7
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 7]
reactivity.tsx:136 IS IT A PROXY {message: 'queryOne,7'}
reactivity.tsx:89 queryOne state success false true false Proxy(Object) {status: 'pending', fetchStatus: 'fetching', isPending: true, isSuccess: false, isError: false, …}
reactivity.tsx:94 queryOne data {message: 'queryOne,7'} {"message":"queryOne,7"}
reactivity.tsx:89 queryOne state success true true false Proxy(Object) {status: 'success', fetchStatus: 'fetching', isPending: false, isSuccess: true, isError: false, …}
reactivity.tsx:94 queryOne data Proxy(Object) {message: 'queryOne,7', Symbol(solid-proxy): Proxy(Object), Symbol(store-node): {…}} {"message":"queryOne,7"}
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 7]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 8]
reactivity.tsx:89 queryOne state pending true false false Proxy(Object) {status: 'pending', fetchStatus: 'fetching', isPending: true, isSuccess: false, isError: false, …}
reactivity.tsx:94 queryOne data undefined undefined
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 8]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 9]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 9]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 10]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 10]
reactivity.tsx:32 queryOne client call (2)Â ['queryOne', 1]
notice the first is it a proxy is not a proxy. Then that it goes back to pending again even though it has data and uses the old data in next load. The UI goes blank on first timer click that navigates to update the ID.
server log:
routeData on server id= 3
prefetch query
queryOne client call [ 'queryOne', 3 ]
queryOne server call [ 'queryOne', 3 ]
routeData on server PRELOAD COMPLETE
IS IT A PROXY { message: 'queryOne,3' }
IS IT A PROXY { message: 'queryOne,3' }
routeData on server id= 7
prefetch query
queryOne client call [ 'queryOne', 7 ]
queryOne server call [ 'queryOne', 7 ]
routeData on server PRELOAD COMPLETE
IS IT A PROXY { message: 'queryOne,7' }
IS IT A PROXY { message: 'queryOne,7' }
we don't see all intermediate calls because of query caching
I chopped the sample down maybe I broke something but even with wrapping the query results in an accessor to force it to call it as a signal it still stops tracking.
@apatrida I think the internals rewrite and the new version has also fixed this issue. I can see these in the logs and I'm hoping this is what you expect?
Here is the working repro https://stackblitz.com/github/ardeora/solid-query-data-as-context-provider?file=src%2Froutes%2Findex.tsx
It is to be noted that Context works differently in SolidJS and if you want updating context values you should pass in a signal instead https://docs.solidjs.com/concepts/context#updating-context-values
I'll be closing this issue for now. Please feel free to reopen it if you still face any issues!