T
TanStack•11mo ago
correct-apricot

How to skip reloading data

Just curious if there's a way to dial in on loaders and skip running fetch functions on every single loader call. Here's a contrived case where using search params I am conditionally rendering a modal, but the root loader also fetches pokemon. It would be fantastic if there was a way for me to do something such that I can skip querying for all pokemon when only the modal search params change.
export const Route = createRootRouteWithContext<{ cache: Cache }>()({
meta: () => [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
component: RootComponent,
validateSearch: z
.object({
modalType: z.enum(["say-hello"]).optional(),
})
.optional().parse,
loaderDeps: ({ search }) => {
return {
modalType: search?.modalType,
};
},
loader: async () => {
return {
allPokemon: await getAllPokemon(),
};
},
notFoundComponent: () => <div>Not Found</div>,
});
export const Route = createRootRouteWithContext<{ cache: Cache }>()({
meta: () => [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
component: RootComponent,
validateSearch: z
.object({
modalType: z.enum(["say-hello"]).optional(),
})
.optional().parse,
loaderDeps: ({ search }) => {
return {
modalType: search?.modalType,
};
},
loader: async () => {
return {
allPokemon: await getAllPokemon(),
};
},
notFoundComponent: () => <div>Not Found</div>,
});
15 Replies
equal-aqua
equal-aqua•11mo ago
why is the modal search param in loader deps if you don't want it to trigger the loader?
correct-apricot
correct-apricotOP•11mo ago
Well I was watching this video yesterday: https://www.youtube.com/watch?v=lcLbYictX3k&t=1s And I was trying to think about whether or not Tanstack basically solved the problem of: If you load all your data at the top of your component tree (in this case at the top of your routers). Can you effectively solve the waterfall problem that pops up when doing fetchOnRender. Mostly the answer is yes... kind of because with Tanstack you can cleanly abstract using useMatch but it also occurs to me that the thing that you lose out on is the ability to get control over when you revalidate specific data in the loader.
Theo - t3․gg
YouTube
The "Wrong Way" To Use React
The "render on fetch" vs "fetch on render" debate has gotten a bit chaotic, and this blog post inspired me to do a deep dive. I hope this is useful to y'all! Sorry for the chaos SOURCE https://bobaekang.com/blog/component-colocation-composition/ MENTIONS https://www.youtube.com/watch?v=SJjK_YWfngU https://x.com/bobaekang TIMESTAMPS 00:00 - No...
equal-aqua
equal-aqua•11mo ago
I don't get your example the only loaderDep you specify is the thing you DONT want to trigger the loader for? so then...just don't? btw I would strongly advise using react query in combination with router
correct-apricot
correct-apricotOP•11mo ago
So the way that the useSuspenseQuery works in tandem with the router is basically by: A: Having the queryprovider's context available in the router B: Having ensureQueryData method being called in the loader Is that correct?
equal-aqua
equal-aqua•11mo ago
what does A) mean exactly? you need to have the queryclient accessible in router, e.g. via router context
correct-apricot
correct-apricotOP•11mo ago
I apologize for the poorly articulated question, I believe I do see what's going on and why react-query indirectly answers the question I had Let me see if I can refactor my example to show it off for other people
// app/routes/index.tsx
import {
createFileRoute,
Link,
useNavigate,
useSearch,
} from "@tanstack/react-router";
import { allPokemonQueryOptions } from "../fetch/pokemon";
import { z } from "zod";
import { useSuspenseQuery } from "@tanstack/react-query";

export const Route = createFileRoute("/")({
component: Home,

loader: async ({ context }) => {
await context.queryClient.ensureQueryData(allPokemonQueryOptions());
},

validateSearch: z
.object({
modalType: z.enum(["say-hello"]).optional(),
})
.optional().parse,
notFoundComponent: () => <div>Not Found</div>,
});

function Home() {
const search = useSearch({ from: "/" });
const { data: allPokemon } = useSuspenseQuery(allPokemonQueryOptions());
const navigate = useNavigate();
return (
<>
{allPokemon?.map((pokemon) => {
return (
<div key={pokemon.name}>
<Link to="/pokemon/$id" params={{ id: pokemon.name }}>
{pokemon.name}
</Link>
</div>
);
})}
{search?.modalType === "say-hello" && (
<button
onClick={() => {
navigate({
to: "/",
search: {},
}).catch(console.error);
}}
>
Hide Modal
</button>
)}

<button
onClick={() => {
navigate({
to: "/",
search: { modalType: "say-hello" },
}).catch(console.error);
}}
>
Say Hello
</button>
</>
);
}
// app/routes/index.tsx
import {
createFileRoute,
Link,
useNavigate,
useSearch,
} from "@tanstack/react-router";
import { allPokemonQueryOptions } from "../fetch/pokemon";
import { z } from "zod";
import { useSuspenseQuery } from "@tanstack/react-query";

export const Route = createFileRoute("/")({
component: Home,

loader: async ({ context }) => {
await context.queryClient.ensureQueryData(allPokemonQueryOptions());
},

validateSearch: z
.object({
modalType: z.enum(["say-hello"]).optional(),
})
.optional().parse,
notFoundComponent: () => <div>Not Found</div>,
});

function Home() {
const search = useSearch({ from: "/" });
const { data: allPokemon } = useSuspenseQuery(allPokemonQueryOptions());
const navigate = useNavigate();
return (
<>
{allPokemon?.map((pokemon) => {
return (
<div key={pokemon.name}>
<Link to="/pokemon/$id" params={{ id: pokemon.name }}>
{pokemon.name}
</Link>
</div>
);
})}
{search?.modalType === "say-hello" && (
<button
onClick={() => {
navigate({
to: "/",
search: {},
}).catch(console.error);
}}
>
Hide Modal
</button>
)}

<button
onClick={() => {
navigate({
to: "/",
search: { modalType: "say-hello" },
}).catch(console.error);
}}
>
Say Hello
</button>
</>
);
}
To be clear ensureQueryData only ensures that there is data there, but DOES NOT REQUERY if it already is. Is that corrrect?
equal-aqua
equal-aqua•11mo ago
depends on the query cache settings I guess
correct-apricot
correct-apricotOP•11mo ago
This DEFINITELY does solve the problem Thanks for your patience @Manuel Schiller
equal-aqua
equal-aqua•11mo ago
more of a rubber duck here, but if it helped 😄
correct-apricot
correct-apricotOP•11mo ago
So what I was playing around with was building something like this:
const cache = new Map()

const loader = () => {
const data1 = smartLoader(
getData: () => getData1(params.one),
key: "data-1",
deps: [params.one] // only get data on first load
)

const data2 = smartLoader(
getData: () => getData2(search.modalType),
key: "data-2",
deps: [search.modalType]
)

return {data1, data2}
}
const cache = new Map()

const loader = () => {
const data1 = smartLoader(
getData: () => getData1(params.one),
key: "data-1",
deps: [params.one] // only get data on first load
)

const data2 = smartLoader(
getData: () => getData2(search.modalType),
key: "data-2",
deps: [search.modalType]
)

return {data1, data2}
}
Then I realized was that I was basically redoing tanstack query just for loaders, and that I was probably doing something stupid
quickest-silver
quickest-silver•11mo ago
i'm facing Cannot read properties of undefined (reading 'ensureQueryData'). May i know any possible reason cause that? I have already followed official tanstack-start example of basic-react-query for setup. Thank you
export const Route = createFileRoute("/_layout/_authed/account")({
component: UserProfile,
validateSearch: (search: Record<string, unknown>): AccountTab => {
// validate and parse the search params into a typed state
return {
tab: (search.tab as ActiveTab) || "account",
};
},
loader: async ({ context }) => {
const data =
await context.queryClient.ensureQueryData(userQueryOptions());

return data;
},
notFoundComponent: () => <div>Not Found</div>,
});
export const Route = createFileRoute("/_layout/_authed/account")({
component: UserProfile,
validateSearch: (search: Record<string, unknown>): AccountTab => {
// validate and parse the search params into a typed state
return {
tab: (search.tab as ActiveTab) || "account",
};
},
loader: async ({ context }) => {
const data =
await context.queryClient.ensureQueryData(userQueryOptions());

return data;
},
notFoundComponent: () => <div>Not Found</div>,
});
equal-aqua
equal-aqua•11mo ago
looks like you are not passing the query client to the router context if you are still struggling please provide a complete minimal example
quickest-silver
quickest-silver•11mo ago
Thank you! Do my setup wrong?
equal-aqua
equal-aqua•11mo ago
we don't support react19 yet so this might be a problem here
quickest-silver
quickest-silver•11mo ago
oh. understood! thank you!

Did you find this page helpful?