Router + Query integration // route invalidation
I'm torn between 2 things:
then either:
which is extremely simple, does not need any loading management of any kind but needs double cache invalidation:
or:
where there's no double cache invalidation needed, but a double api declaration (the query options in both loader and useSuspenseQuery)
what i could see as a workaround is:
but i don't know how to type
params and search. I have tried ReturnType<(typeof Route)['useParams']> but it returns any 😄
...and it's still not as appealing DX as the very 1st one, which only have an issue of route caching data on top of react-query caching data (and subscriptions)
please advice 🙏28 Replies
exotic-emeraldOP•5w ago
(mentioning @TkDodo 🔮 only because you told me to join discord on X)e
btw I understand i may be missing on subscriptions since we don't use
useQuery/useSuspenseQuery therefore queryClient.setQueryData() would not trigger a re-render. I'm also considering this in the balance.correct-apricot•5w ago
I think the best approach is to not
useLoaderData if you use query, otherwise, what do you have it for? You could just as well call fetch() in the loader without it as caching doesn't really do anything. okay, retries and stuff, but still.
If you want loader data, don't use ensureQueryData because it never refetches - just do queryClient.fetchQuery and maybe pass a staleTime: 0 explicitly to always force a fetch when the loader runs. Then, you can get rid of queryClient.invalidateQueries because invalidating the router will run the loader, which will always execute the query then.
However, not having a subscription with useQuery or useSuspenseQuery makes the query eligible for garbage collection, and invalidateQueries won't refetch it because the query is not active. So I'm not even sure how the double invalidation would get you fresh data because router invalidation runs the loader, but ensureQueryData only returns what is in the cache already and invalidateQueries doesn't refetch either 🤔
So, really the best way would be just loader + useSuspenseQuery, then just do normal invalidation. That's not a workaround, imo it's the best possible usage. The drawback is you "duplicate" things between loader and component, but there's actually a nice way around this with router context.
I'm not supposed to talk about this because the API is undocumented and will change, but the concept is:
then in the component:
correct-apricot•5w ago
I'm doing this in my react-query workshop, have a look here:
https://github.com/TkDodo/react-query-beyond-the-basics/blob/a77f3d1f1f6686307f1e44079c839d049fbe48ca/src/routes/index.tsx#L16-L22
GitHub
react-query-beyond-the-basics/src/routes/index.tsx at a77f3d1f1f668...
Contribute to TkDodo/react-query-beyond-the-basics development by creating an account on GitHub.
correct-apricot•5w ago
Again, the
context feature is currently undocumented because we want to rename it, so use with caution. You could use beforeLoad too but it's not as optimized for this use-caserobust-apricot•5w ago
correct-apricot•5w ago
just ship the rename 😂
robust-apricot•5w ago
lol
just implement the whole concept!
correct-apricot•5w ago
the concept already works like it should for router is my point. There's nothing missing afaik, it's "only" for start
exotic-emeraldOP•5w ago
@TkDodo 🔮 understood. it's indeed a good idea to use the context to build the query, it'd solve the typing issue
i've a follow up question. the router loader is also nice because you can compose multiple sources, but that can't be done with a single suspense query (and afaik we should not chain suspense queries right?)
you can imagine 2 fetches in parallel, let's say "user profile by id" and "user memberships by id"
i have found that i could not rely on context(), but beforeLoad() works ok.
i have a routes such as:
/$orgSlug/route.tsx
/$orgSlug/locations/$locationId.tsx
1st one does a BE check to fetch the organization by slug and returns { organizationId } ; i went with beforeLoad but i'm not sure if it should be loader. it can't be context because it's using async calls to BE
then if using loaders, i need to work for building the queryOptions and fetching data in sub route loaders, so it sucks (imho). so i went with beforeLoad.
/$orgSlug/route.tsx
/$orgSlug/locations/$locationId.tsx
this seems to work ok in terms of cascading
(on this, the API is a bit quirky:
context and loader get deps, but beforeLoad gets search - i feel like it's related to the loader's cache key, but it feels odd)
also using trpc, there are type issues in passing the queryOptions to the context :/ (had to declaration: false it)
i've moved further, and the pattern also gets nastier when you want conditional loading. with loader, it's so easy:
so that we can do "pre-filled" forms if/when param exists in searchParams
with RQ i first thought to do:
but if queryOptions is undefined, i can't do:
so i have to:
and
which doesn't seem like a good DX at all (more verbose + useEffect). i'm sure i'm missing something
@TkDodo 🔮 would you have an example of such in your workshop maybe 🙏 ?correct-apricot•5w ago
i have found that i could not rely on context(), but beforeLoad() works ok.why not? I would just use a
skipToken for that. Raw RQ:
then just consume as usual. I'm sure trpc has a way to pass skipTokenexotic-emeraldOP•5w ago
because i need to load things to expose them in context, but context is not async
there's no elegant way to do this when using trpc or RQ wrappers. i'd need to write:
i guess all the types would be inferred from the queryFn return type so i would have to check that queryoptions.queryFn isn't undefined to get proper typing in component
correct-apricot•5w ago
yes context is synchronous but the idea is to just put
queryOptions in there which is also sync ? There's nothing async in the exmples you've shown so far...correct-apricot•5w ago
Disabling Queries | tRPC
To disable queries, you can pass skipToken as the first argument to useQuery or useInfiniteQuery. This will prevent the query from being executed.
correct-apricot•5w ago
so it would be:
exotic-emeraldOP•4w ago
thanks for this 🙏
so, i kinda did but i can see it's very deep nested and not obvious.
i have a
/$orgSlug/route.tsx which acts as organization guard. it async calls getOrgIdBySlug in beforeLoad and returns { organizationId } so this gets added to the context.
in my sub-routes, i need that organizationId to build the queryOptions but it's not available in context because the context call seems to be calls before the beforeLoad of the $slug route is finished.exotic-emeraldOP•4w ago

correct-apricot•4w ago
yeah context runs beforeLoad
exotic-emeraldOP•4w ago


correct-apricot•4w ago
beforeLoad is blocking and runs on every navigation, so doing something async there is usually only recommended for auth and/or if you have it exceissively cached
exotic-emeraldOP•4w ago
that is exactly what we do (auth :D)
so that's why i can't use context for building the queryOptions 🙁
i could build the queryOptions in the loader and return it, but then it wouldn't be in
useRouteContext but rather in useLoaderDatacorrect-apricot•4w ago
yeah it's interesting that you have
slug in the path but the API needs id. I'd probably try to make getForm accept a slug and then just do the lookup in the backend or so. but you can also use beforeLoad to build queryOptions, the downside is that it's not memoized so it runs on every navigation, giving you new queryOptions, and then useRoutContext will re-render all subscribers. context is "better" for this because it depends on loaderDeps so it will not re-run for unrelated query param changesexotic-emeraldOP•4w ago
we have slug in url just for having human readable urls.. but ids are stored in db on every object or so to scope them by tenant. we use nosql db so id is binary format (ObjectId) so not text for faster indexing, db queries etc etc...
i understand the implications better, what you explained is crystal clear
but unfortunately i don't think i can swap slug to id or do differently. i prefer to overhead client rendering a bit knowing that react-query caches result anyway, rather than adding a "getIdBySlug" for every single request i process in BE
(it's outside of scope but DB is already a bit far from BE so timings are never below 100ms which is bothering me -_- having one more request to process organizationId at the start of a handler would add a lot)
exotic-emeraldOP•4w ago
(btw if you're ever interested in a feedback, this doesn't work). it seems that
ensureQueryData does not support skipToken (but maybe that's expected and that's just me)

exotic-emeraldOP•4w ago
best looking attempt is:

correct-apricot•4w ago
oh right, the expectation for declarative functions is that you just don't cal them, that's why enabled isn't supported and skipToken is just an extension of enabled. But I think we need to start checking for this with v6 because yeah, that's not a good experience!
exotic-emeraldOP•4w ago
i understand that it would not be possible (easily), but imho best would be to keep the router loader DX, it's much more powerful and concise in writing. the problem is the reactivity with react-query and the propagation to route hooks. great challenge 😬
@TkDodo 🔮 if i use
useSuspenseQuery and i can't use Route.useLoaderData then, what's the benefit of the route loader part? i mean what's the benefits of:
vs
it seems to me it would do the same, no?correct-apricot•4w ago
Fetching starts earlier
With intent prefetching, the loader even runs when you hover the link to the page
exotic-emeraldOP•4w ago
mmm i see
thanks 🙂