T
TanStack•7mo ago
xenial-black

ensureQueryData in loader

I'm using Start (^1.105.3) + Query (^5.66.3) and I'm trying to ensure query data in the loader Argument in createFileRoute. The problem is that I get Unexpected end on JSON input when I reload the page (i.e server rendered) but it works when I'm loading a page without the loader and then navigate to one which has it. Thanks in advance createRouter:
export function createRouter() {
const queryClient = new QueryClient();

const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
context: {
queryClient,
},
scrollRestoration: true,
defaultPreload: "intent",
}),
queryClient,
);

return router;
}
export function createRouter() {
const queryClient = new QueryClient();

const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
context: {
queryClient,
},
scrollRestoration: true,
defaultPreload: "intent",
}),
queryClient,
);

return router;
}
$projectId.tsx:
export const Route = createFileRoute("/_authed/projects/$projectId/")({
component: RouteComponent,
loader: async ({ context, params }) => {
await context.queryClient.ensureQueryData(
projectQueryOptions(params.projectId),
);
},
});
export const Route = createFileRoute("/_authed/projects/$projectId/")({
component: RouteComponent,
loader: async ({ context, params }) => {
await context.queryClient.ensureQueryData(
projectQueryOptions(params.projectId),
);
},
});
projectQueryOptions:
export const projectQueryOptions = (projectId: string) =>
queryOptions({
queryKey: ["projects", projectId],
queryFn: async ({ signal }) => {
const res = await client.api.projects[":projectId"].$get(
{
param: {
projectId,
},
},
{
init: {
signal,
},
},
);
return await res.json();
},
});
export const projectQueryOptions = (projectId: string) =>
queryOptions({
queryKey: ["projects", projectId],
queryFn: async ({ signal }) => {
const res = await client.api.projects[":projectId"].$get(
{
param: {
projectId,
},
},
{
init: {
signal,
},
},
);
return await res.json();
},
});
24 Replies
harsh-harlequin
harsh-harlequin•7mo ago
can the queryFn be executed on the server during SSR? so is this initialized correctly? client.api.projects[":projectId"].$get
xenial-black
xenial-blackOP•7mo ago
Quick idea how I could check? The call itself is correct, it's Hono RPC
harsh-harlequin
harsh-harlequin•7mo ago
Quick idea how I could check?
call it from a server function maybe? but thats not the same as SSR so how do you create that client?
xenial-black
xenial-blackOP•7mo ago
It's exported from my Hono app and uses the cookies as authorization, maybe that's the problem now that I think about it... Although, is it? I mean I request the Tanstack Start Server with the credentials included, but I don't know if they're passed to my client.. Any idea where I could look on how to implement this right? I probably need to create a seperate instance for the server rendering / my instance needs to supports both
harsh-harlequin
harsh-harlequin•7mo ago
you probably would need to do a getCookie('your-cookie') on the server during SSR to pass the cookie on it really depends how your client reads that cookie right now or what it expects
xenial-black
xenial-blackOP•7mo ago
It passes it on with credentials: "include" currently
harsh-harlequin
harsh-harlequin•7mo ago
how does that look like?
xenial-black
xenial-blackOP•7mo ago
This is just a wrapped fetch from Hono for the trpc-like architecture
export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
});
export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
});
This seems to work for the SSR side (at least it doesn't crash anymore) but of course doesn't work on the client side
export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
});
export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
});
harsh-harlequin
harsh-harlequin•7mo ago
where is that client created?
xenial-black
xenial-blackOP•7mo ago
In my Start app and used in queryFn's So basically SSR and CSR
harsh-harlequin
harsh-harlequin•7mo ago
one solution could be to put the client into router context. in your createRouter function in router.tsx (i assume you have the default files from the examples), add an arg:
export function createRouter(client: SomeType) {
const router = createTanStackRouter({
routeTree,
context: { client } // <<<<<<
})

return router
}
export function createRouter(client: SomeType) {
const router = createTanStackRouter({
routeTree,
context: { client } // <<<<<<
})

return router
}
then in ssr.tsx:
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'

import { createRouter } from './router'

const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
});

export default createStartHandler({
createRouter: () => createRouter(client)
getRouterManifest,
})(defaultStreamHandler)
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'

import { createRouter } from './router'

const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
});

export default createStartHandler({
createRouter: () => createRouter(client)
getRouterManifest,
})(defaultStreamHandler)
and in client.tsx
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'

export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
});

const router = createRouter(client)

hydrateRoot(document, <StartClient router={router} />)
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'

export const client = hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
});

const router = createRouter(client)

hydrateRoot(document, <StartClient router={router} />)
xenial-black
xenial-blackOP•7mo ago
This seems to work I've added it as described but I also have Query in my router.tsx which wraps createTanStackRouter with routerWithQueryClient and the type check of routerWithQueryClient seems to hijack the context-type and throws an error: Object literal may only specify known properties, and 'client' does not exist in type '{ queryClient: QueryClient; } When ignoring for know (bad idea, I know) and passing it to my query options I did this:
export const projectsQueryOptions = (ssrClient?: Client) => {
const c = ssrClient ?? client;
return queryOptions({
queryKey: ["projects"],
queryFn: async ({ signal }) => {
const res = await c.api.projects.$get(
{},
{
init: {
signal,
},
},
);
return await res.json();
},
});
};
export const projectsQueryOptions = (ssrClient?: Client) => {
const c = ssrClient ?? client;
return queryOptions({
queryKey: ["projects"],
queryFn: async ({ signal }) => {
const res = await c.api.projects.$get(
{},
{
init: {
signal,
},
},
);
return await res.json();
},
});
};
And this seems to work! Any idea how I can fix the type error in context?
harsh-harlequin
harsh-harlequin•7mo ago
whats the type arg of createRootRouteWithContext?
xenial-black
xenial-blackOP•7mo ago
Ohhh! This fixed it! Thank you! This type magic takes a bit getting used to really :D
harsh-harlequin
harsh-harlequin•7mo ago
😄
xenial-black
xenial-blackOP•7mo ago
While I have you here, is there a better way than to pass the client as an argument to every ...QueryOptions or is this the best way for this problem?
harsh-harlequin
harsh-harlequin•7mo ago
show me some examples where you do that repeatedly
xenial-black
xenial-blackOP•7mo ago
A lot of my pages would look like this now:
export const Route = createFileRoute("/_authed/projects/")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(
projectsQueryOptions(context.client),
);
},
component: RouteComponent,
});

function RouteComponent({ context }: { context: RouteContext }) {
const { client } = useRouteContext({ from: "/_authed/projects/" });
const { data, isPending } = useProjects(client);
...
}
export const Route = createFileRoute("/_authed/projects/")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(
projectsQueryOptions(context.client),
);
},
component: RouteComponent,
});

function RouteComponent({ context }: { context: RouteContext }) {
const { client } = useRouteContext({ from: "/_authed/projects/" });
const { data, isPending } = useProjects(client);
...
}
harsh-harlequin
harsh-harlequin•7mo ago
i see. a different way would be to not put the client into router context, but instead define a function getHonoClient with different implementations for client and server.
import { createIsomorphicFn } from '@tanstack/start'

const getHonoClient = createIsomorphicFn()
.client(() => hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
}))
.server(() => hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
}))
import { createIsomorphicFn } from '@tanstack/start'

const getHonoClient = createIsomorphicFn()
.client(() => hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
}))
.server(() => hcWithType("http://localhost:1337", {
init: DEFAULT_INIT,
headers: {
cookie: getCookie("better-auth.session_token") ?? "",
},
}))
and then call getHonoClient() wherever you need access to the client
xenial-black
xenial-blackOP•7mo ago
Oh, you thought of everything huh? Will try! Thank you very much for your help! Yeah, this works beautifully and looks even good, thank you!
harsh-harlequin
harsh-harlequin•7mo ago
nice just as an FYI, why the createIsomorphicFn() is necessary. if you would do the classic if (typeof document === "undefined") runtime check, the getCookie function (and all it depends upon) would end up in the client bundle
xenial-black
xenial-blackOP•7mo ago
I love SSR so much but I hate it for stuff like this so much aswell 😄
harsh-harlequin
harsh-harlequin•7mo ago
yeah, unfortunately sometimes the abstractions leak ... and then you need an escape hatch as this but luckily it does not happen that often
xenial-black
xenial-blackOP•7mo ago
Yeah, but this solution is beautiful, I really like this. It makes so much sense and everyone will understand it in the future and that's perfect!

Did you find this page helpful?