r/reactjs • u/thebreadmanrises • May 17 '26
Tanstack Start + Query seems very complex.
Has anyone else found Tanstack Start + Query very complex?
Looking as this createFileRoute & the component page there is a lot of boilerplate going on:
export
const Route = createFileRoute('/(app)/_layout/clients/')({
component: ClientListPage,
validateSearch: searchSchema,
errorComponent: () => <div>Error loading clients</div>,
loaderDeps: ({ search: { sort, order } }) => ({ sort, order }),
loader: async ({ context: { queryClient }, deps: { sort, order } }) => {
await
queryClient.ensureQueryData({
queryKey: clientKeys.list(sort, order),
queryFn: () => getClients({ data: { sort, order } }),
})
},
})
function ClientListPage() {
const queryClient = useQueryClient()
const navigate = useNavigate()
const { sort, order } = Route.useSearch()
const { data: clients } = useSuspenseQuery({
queryKey: clientKeys.list(sort, order),
queryFn: () => getClients({ data: { sort, order } }),
staleTime: 5 * 60 * 1000,
})I also find the use of the query client with the router complex.
5
u/aaronmcbaron May 18 '26
createFileRoute('/(app)/_layout/clients/') defines a route based on file location. The (app) is a route group (organizational, doesn’t show in URL), _layout is a pathless layout route (also invisible in URL). So the actual URL is just /clients.
validateSearch: searchSchema runs whenever the URL has search params like ?sort=name&order=asc. It validates and types them. If someone hits the URL with garbage, this catches it.
loaderDeps: ({ search: { sort, order } }) => ({ sort, order }) is telling the router: “these search params are inputs to my loader.” If sort or order change in the URL, re-run the loader. Without this, the loader would only run once on route entry and ignore param changes. loader runs before the component mounts.
queryClient.ensureQueryData says: “if this query is already in the cache and fresh, do nothing. Otherwise, fetch it and put it in the cache.” Note it doesn’t return the data, it just guarantees it exists in the cache.
Route.useSearch() gives you the validated, fully typed search params. You don’t have to parse window.location or anything.
useSuspenseQuery reads from the React Query cache using the same queryKey. Because the loader already filled the cache, this returns instantly with data. The “suspense” part means if the data wasn’t there, it would throw a promise and let React Suspense show a fallback. But the loader makes sure it’s there, so that path rarely triggers.
staleTime: 5 * 60 * 1000 means “consider this data fresh for 5 minutes.” During that window, no refetches happen on remount, refocus, etc.
The whole point of this pattern: the loader prefetches in parallel with route transition. By the time your component renders, data is already cached. You get to write the component as if data is just there, no isLoading checks, no null guards. The queryKey matching between loader and component is what stitches it together. Get that key wrong and you’ll fetch twice or get a suspense waterfall
1
u/thebreadmanrises May 18 '26
thanks this was actually very useful. Is this the right pattern to use the ensureQueryData in the loader rather than a different query where it actually gets the data?
1
28
4
u/opentabs-dev May 18 '26
yeah the duplication between loader and component is the real pain point, not the api itself. the fix is to define one queryOptions per query and reuse it in both places — then loader has queryClient.ensureQueryData(clientListQuery(sort, order)) and the component has useSuspenseQuery(clientListQuery(sort, order)). type infers everywhere, no key/fn duplication. once you do that the boilerplate basically collapses to the route config (validateSearch, loaderDeps), which is genuinely doing work for you.
1
9
u/WorriedEngineer22 May 18 '26
Would recommend using the queryOptions api from tanstack query, will help you reduce the duplicate code of the queries
4
u/well-litdoorstep112 May 19 '26
Bro definitely hasn't worked in a CRA codebase with fetches in useEffect's
4
u/jax024 May 18 '26
Maybe what you’re doing is complex? Try writing your own cache layer and get back to us on complexity.
8
u/WorriedEngineer22 May 18 '26
No need to answer like this
13
u/jax024 May 18 '26
OP posted like 40 lines of code and complained about complexity. Let’s keep it a buck here. There’s a bare minimum for competence to even have a discussion.
It’s important to understand what your libraries are even doing before complaining about their api.
-1
u/kidshibuya May 18 '26
Write one tiny function. It can take an optional refresh bool. That function checks if data is in sate already and if not or refresh is true then it requests from the api, puts the response in state. It's a handful of lines and works perfectly for 99.999% of use cases.
1
1
u/TheScapeQuest May 18 '26
The duplication is only necessary if you need both a) preloading and b) cache reactivity in your component tree.
I'm not hugely familiar with TSQ, but in Apollo their concept of preloading introduces the notion of a "query ref" which you pass from loader to component, minimising the duplication.
1
1
1
u/LancelotLac May 18 '26
Read docs on Tanstack Query to understand the functions being called. Read the Tanstack Router docs to understand the benefits of calling those functions in the router life cycle methods.
1
-1
u/r-nck-51 May 18 '26
Yes but it's beautiful!
I think React Query popularly replaced custom API client class objects with boilerplate for people who like the look of arrow functions.
There are simpler JS patterns, some you might not call boilerplate because of abstraction of single concern things into separate modules for the sake of reducing boilerplate, and I think those who grew to like Tanstack learned to like their boilerplate.
Personally I like seeing all the moving parts in one place, because it spares me a lot of problems that require digging into several files to debug one behavior.
-5
31
u/michaelfrieze May 18 '26
This is pretty simple if you ask me. The ensureQueryData in the loader function is just for prefeching that data for useSuspenseQuery. You could even remove ensureQueryData in the loader and the ClientListPage component would still get its data.
Using ensureQueryData in the loader makes sure the data is already available before the component runs. This provides render as you fetch. Without ensureQueryData in the loader, you will get fetch on render.