Using beforeLoad to inject data into context for deeply nested routes has performance issues
In our application, we have routes that are pretty deeply nested that need access to the context from the parent routes.
For example, consider the following:
https://example.com/dashboard/users/$userId/cars/$carId/parts/$partId
The leaf route (/$partId) is it's own page with an index route. It does not get anything rendered from layout routes from any of the parents. In this page, we may want to render a title that says John Doe-Toyota Corolla-Lug Nut.
To do this, we are currently fetching the relevant data in each parent. /dashboard/users/$userId gets the user object and injects it into the context for itself and children. $carId does the same for itself, etc. This works great and makes our data access very simple. No need to have util files with fetcher functions or anything else. Simply call the api client on the beforeLoad of the parent and get access to it everywhere.
However, this is very unperformant. On the parts page, every time you click a new part in the table, it fetches:
/users All users
/users/$userId More info on that one user
/users/$userId/cars All cars for that user
/users/$userId/cars/$carId More info on that car
/users/$userId/cars/$carId/parts All parts for that car
/users/$userId/cars/$carId/parts/$partId More info for that part
All this in addition to any auth checks, etc. done in /dashboard. Is there no way to cache data in beforeLoad? Or any other pattern which makes accessing data like this easier and more performant? The Tanstack Router documents lack an example like this where data is deeply nested and access to data from very high up in the tree may be needed at the bottom.
11 Replies
extended-salmon•2w ago
why do all of the nested routes need the parent data?
exotic-emeraldOP•2w ago
One example I mentioned in the post is if we want to display data from the parent as a title.
e.g.
John Doe-Toyota Corolla-Lug Nut.
John Doe
is from $userId
Toyotoa Corolla
is from $carId
Lug Nut
is from $partId
Another example could be an "Buy Now" button on the deeply nested route that needs to know the address of the user, which may have been returned earlire on the user pageextended-salmon•2w ago
I wouldn't use route context for this though
this leads to a waterfall
if you don't need the data in router itself but only when rendering, tanstack query seems much more suitable
exotic-emeraldOP•2w ago
this leads to a waterfallright. is it not on the roadmap to implement caching or only checking the parts of the route that change? tanstack query is suitable, but results in pretty messy code + calling queries over and over in every component. in the
$userId
example, we would now need to call the same query over and over on permutation of child pages
the main reason we went with tanstack router for our use case was because of how simple and clean it made our code and dependency graphlike-gold•2w ago
my understanding:
- the "data" that is returned in
beforeLoad
isn't automatically cached
- data that is returned in loader
is cached by the router
- it's better to use beforeLoad
for things like auth, redirects, and setting up a context for routes to consume
- beforeLoad
functions are called serially as described here while loader
functions are called in parallel
I'm assuming you're opting to use beforeLoad
instead of loader
because of this last point, as your partId
route depends on the "car" it belongs to, and the "car" depends on the "user" that car belongs to. so you want the data to be loaded serially
I don't think there's a plan for the return value of beforeLoad
to be cached. if you want caching within this lifecycle hook, you need to do it yourself, which isn't too hard. however, you're working against the library by using beforeLoad
for caching instead of loader
. so IMO you have 2 options:
1. Cache the data in beforeLoad
using your own caching mechanism – I do this using tanstack-query for data that is needed at the root level, or....
2. (preferred) Use the loader
function to cache your data, and modify your backend API appropriately. I would opt to use a single API route for e.g. /users/$userId/cars/$carId/parts/$partId
and that (backend) route would load all of the data necessary for the UI. this reduces backend load significantly
tanstack query is suitable, but results in pretty messy code + calling queries over and over in every component. in the $userId example, we would now need to call the same query over and over on permutation of child pagescan you share what the code you're writing looks like? I find the tanstack-query code to be extremely clean, way way better than what I had used in the past
exotic-emeraldOP•2w ago
i agree tanstack query is pretty clean out of the box, i think the "messiness" comes from calling the same query over and over across different pages. way cleaner to just have the data available to you in a context
for option 1, i think i could maybe use
ensureQueryData
?
that would keep the existing pattern and integrate tanstack query to a certain point. and avoid the hooks being called over and over in components
agreed maybe i'm using an antipattern in tanstack router, however other than the performance issues it makes a lot of sense for our use case. very clean and straight forward and lets us move very fast when building FE features, especially since we don't need to implement option 2 where we are pulling the same permutations of data slightly differently per routexenophobic-harlequin•2w ago
What's messy about it?
ensureQueryData
in loaders and call queries at the route component level, then pass the data down the tree. That's itexotic-emeraldOP•2w ago
How would you pass data down the tree?
In this example, I can
ensureQueryData
in userId
and call the query in my user route component. Do I now have to call the query at every child that needs it? Now I need to start exporting my query options across files for every single query no?like-gold•2w ago
it's not particularly pretty but yes that is what I started to do. it's similar in nature to what TkDodo writes about in his blog post here. effectively for each data source that I want to use multiple "kinds of query functions" with, I export a
create<MyResource>QueryOptions
function. (by "kinds of query functions" I mean e.g. ensureQueryData, useQuery, prefetchQuery...)conscious-sapphire•2d ago
Hi, I just realised today about this behavior, I've been using
beforeLoad
to load user data not expecting it to be reloaded on every route/search change (I started using search params today and this causes me a lot of problems)
Im struggling to understand the best approach, the docs do exactly as I did (https://tanstack.com/start/latest/docs/framework/react/examples/start-basic-auth?path=examples%2Freact%2Fstart-basic-auth%2Fsrc%2Froutes%2F__root.tsx)
I tried using a queryClient initialized in the context but then the invalidation doesnt work correctly, the beforeLoad call seems to still have the cached data, failing to redirect and remove data at all
Any help ?mute-gold•2d ago
I think your
beforeLoad
is being cached. The docs recommend setting defaultPreloadStaleTime: 0
if using an external data loading/caching library https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#passing-all-loader-events-to-an-external-cacheData Loading | TanStack Router React Docs
Data loading is a common concern for web applications and is related to routing. When loading a page for your app, it's ideal if all of the page's async requirements are fetched and fulfilled as early...