T
TanStackโ€ข2y ago
like-gold

understanding layout routes

I'm having a hard time understanding layout routes. Maybe my use-case is a bit non-standard, but what I would like to achieve is the following scenario. We have a lot of routes per "workspace" that live under /workspaces/$id, for example:
/workspaces/$id/help
/workspaces/$id/dashboard
/workspaces/$id/help
/workspaces/$id/dashboard
etc. There are also routes outside of workspaces/$id, but those that are under it should have a shared layout (mainly headerBar + sideBar + some data fetching + making sure the $id is a valid uuid etc.) I thought about the following, file-based structure:
- routes
- __root.tsx
- workspaces
- $id
- help.tsx
- dashboard.tsx
- route.tsx
- routes
- __root.tsx
- workspaces
- $id
- help.tsx
- dashboard.tsx
- route.tsx
this "works", but it's not really a layout, or is it ? The route.tsx is where I would parseParams and render header + sideBar, because it will be rendered for all pages below it. One problem is that I then can additionally create urls that go to="/workspaces/$id" and it's seen as valid by the router, which I wouldn't really like because it is not a page that actually exists. So I thought about adding an index.tsx route that redirects to the help page, and that works fine at runtime, but now the router allows the following links to be created on type level:
'/workspaces/$id' | '/workspaces/$id/help' | '/workspaces/$id/dashboard' | '/workspaces/id/' | '/'
'/workspaces/$id' | '/workspaces/$id/help' | '/workspaces/$id/dashboard' | '/workspaces/id/' | '/'
I'm not really sure why we have $id link twice in ther now - once with slash and once without ๐Ÿ˜… . Since we are using trailingSlash: 'always', the created links are really the same. I think the root cause is that after creating route.tsx, we get a valid link towards '/workspaces/$id', which shouldn't be the case because route.tsx isn't really a page...
65 Replies
like-gold
like-goldOPโ€ข2y ago
another thing I have tried is really using layouts, with the following structure:
- routes
- __root.tsx
- workspaces
- $id
- _layout
- help.tsx
- dashboard.tsx
- _layout.tsx
- index.tsx
- routes
- __root.tsx
- workspaces
- $id
- _layout
- help.tsx
- dashboard.tsx
- _layout.tsx
- index.tsx
so instead of route.tsx, there is _layout.tsx and then in the directory of the layout, there are the sub routes. This pretty much has the same problem - I can link to '/workspaces/$id' even though I think this route shouldn't exist, and there is another issue: Inside _layout.tsx, I can't parse params because the params I'm receiving are of type Record<never, string> instead of Record<'id', string> as they were before with route.tsx
parseParams: (params) => ({
id: z.string().uuid().parse(params.id),
}),
parseParams: (params) => ({
id: z.string().uuid().parse(params.id),
}),
I can create a reproduction for this if you think this is just a typings issue - I was just wondering which approach would be the recommended one in general since they both kind of achieve the same thing - having a route.tsx file or a _layout.tsx file ... thank you ๐Ÿ™
manual-pink
manual-pinkโ€ข2y ago
a repro for the second one would be great generally speaking, a route.tsx will cause the route to exist and thus be a valid link target in your second approach that uses a real (as in underscore prefix ) layout, /workspaces/$id should not exist as a route. maybe there is a bug in the generator (virtual route being created?)
like-gold
like-goldOPโ€ข2y ago
Yes I see a virtual route being created in the routeTree generated file. I'll do a reproduction tomorrow
like-gold
like-goldOPโ€ข2y ago
here's the reproduction for the _layout structure, and it shows both issues: https://stackblitz.com/edit/tanstack-router-23xrx1?file=src%2Froutes%2Fworkspaces%2F%24id%2F_layout.tsx 1. in /workspaces/$id/_layout, params passed into parseParams are of type Record<never, string> rather than Record<'id', string> 2. you can see in __root.tsx that I can create links towards both /workspaces/$id and /workspaces/$id/, even though they don't exist. Navigating to one of those links in the browser shows both routes as active, and they just render an empty page.
Dominik Dorfmeister
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
like-gold
like-goldOPโ€ข2y ago
@Manuel Schiller FYI โฌ†๏ธ let me know if I should file an issue for this ๐Ÿ™‚
manual-pink
manual-pinkโ€ข2y ago
I found the reason why you would get never when trying to parse the params in the _layout route. while we could "fix" this, I think we should fundamentally discuss the structure you want to achieve. @Tanner Linsley we are missing the "opposite" of a layout route: a route that contributes to the path but cannot be matched on its own
absent-sapphire
absent-sapphireโ€ข2y ago
Can you elaborate or explain the use case? When doing code base routing, isnโ€™t this simply having a prefix or a suffix or multiple path segments in your path? And if so, I can see how it would be extremely difficult for us to design this in file-based routing,
manual-pink
manual-pinkโ€ข2y ago
see the use case presented by @TkDodo ๐Ÿ”ฎ he wants to group a set of routes under /workspaces/$id, but /workspaces/$id itself should not be a valid route
absent-sapphire
absent-sapphireโ€ข2y ago
You mean an index route on that? The root is valid, just not that exact match? Seems like a redirect would solve this Either detect the lack of a sub route in the layout and redirect. Or add an index route and redirect from there. Technically, you could create a bunch of routes in code routing with path: '/workspaces/$id/whatever', which would only match if it was complete down to the whatever
like-gold
like-goldOPโ€ข2y ago
Yeah I have an index route and redirect from there, it's parseParams that doesn't work then on type level, and I get two different valid routes: /workspaces/$id and /workspaces/$id/. The one with trailing slash comes from the index route, but why is the other one there?
absent-sapphire
absent-sapphireโ€ข2y ago
Technically speaking, what you're looking for is some kind of marker that will tell the internal matching system to skip over it as an index match and keep going The other one being there sounds like a type bug At one point I had some type logic in place that would add trailing slashes to everything for path matching
like-gold
like-goldOPโ€ข2y ago
Yeah @Manuel Schiller also said it likely shouldn't be there
absent-sapphire
absent-sapphireโ€ข2y ago
However, it's still relevant for routes/matches based on ID e.g., You want to target the the loader data for that layout route So navigation... it shouldn't be there But for things like data, context, acquiring routes/matches by ID, it does
like-gold
like-goldOPโ€ข2y ago
Oh yeah totally, it's only weird for navigation I also don't fully understand if the route.tsx approach is better or the layout one. I feel that with route.tsx, I get what I want with less effort / fewer files
absent-sapphire
absent-sapphireโ€ข2y ago
Example?
manual-pink
manual-pinkโ€ข2y ago
you would not need the index route right? route.tsx should suffice for the redirecting
like-gold
like-goldOPโ€ข2y ago
The two route trees I've shown in the initial post I can't redirect in route.tsx because it's also rendered for sub-routes I thought...
absent-sapphire
absent-sapphireโ€ข2y ago
Either works Route.tsx is not an index route. Itโ€™s just the same as making a route with the same name as as the directory above it
manual-pink
manual-pinkโ€ข2y ago
you should be able to redirect conditionally
absent-sapphire
absent-sapphireโ€ข2y ago
Correct It all just depends on how you want to approach it.
manual-pink
manual-pinkโ€ข2y ago
that's what I meant as " the opposite of layout routes"
absent-sapphire
absent-sapphireโ€ข2y ago
Yeah, adding that marker wouldnโ€™t be hard Honestly though, it should already work that way Like, unless you have an index route, it shouldnโ€™t consider the branch as a destination to match Types, yes we need to check those. But runtime? I think thatโ€™s already how it works.
manual-pink
manual-pinkโ€ข2y ago
for types to work, we would need to check if a route is a leaf? to be a valid target
absent-sapphire
absent-sapphireโ€ข2y ago
Yes. For navigation But there are places where branches are valid as well.
manual-pink
manual-pinkโ€ข2y ago
where?
absent-sapphire
absent-sapphireโ€ข2y ago
When you write a link with a from thatโ€™s originating from a layout route, technically from: /branch is valid getRouteApi() can use branches useSearch from a branch is technically possible if you donโ€™t know what leaf youโ€™re on but what access to the shared search
manual-pink
manual-pinkโ€ข2y ago
but to?
absent-sapphire
absent-sapphireโ€ข2y ago
It's really just to: ... that needs to be locked down I think Again, at one point this was implemented But as we needed to fix more important bugs, I think some of this nuance was lost and no type tests ๐Ÿคฆโ€โ™‚๏ธ To would only allow and autocomplet to leafs, but from can come from any valid branch/leaf
manual-pink
manual-pinkโ€ข2y ago
sounds like a job for @Chris Horobin ๐Ÿ˜
absent-sapphire
absent-sapphireโ€ข2y ago
Indeed ๐Ÿ™‚ @Chris Horobin You up for the challenge? (not to say it would even be remotely difficult for you)
ambitious-aqua
ambitious-aquaโ€ข2y ago
I've been summoned. I'm reading back through the messages. From what I understand we basically should only consider leaves as destinations. But from can consider branches? Because you might want to be more loose about that I could be coming from any of these leaves under a branch? But a destination has to be concretely a leaf? There's a lot of nuance here so we definitely need type tests to cover this Please let me know if I misunderstood this ๐Ÿ˜…
manual-pink
manual-pinkโ€ข2y ago
perfect summary
ambitious-aqua
ambitious-aquaโ€ข2y ago
yeah. I will think about an efficient way to do the typing of this. From a type level point of view, it might only know if it is a leaf when there are no children and that might be the most efficient anyway. Parsing the actual paths to work this out might not be a good idea Like, each route knows if it has children or not. A path does not know without either looking up the route by RouteByPath or looking for a prefix in a huge union (this option is a bad idea imo) Maybe how I would do it is make an extra type on top of RoutePaths called RouteLeaves which extracts routes that are leaves. Sound good?
manual-pink
manual-pinkโ€ข2y ago
maybe already think about a type that could be generated by the generator as well as calculated by TS for code based routing
ambitious-aqua
ambitious-aquaโ€ข2y ago
This is probably a separate topic I'm currently investigating. Theres definitely some very easy things like path, id, fullPath that can be added to the generated types to make TS faster (as string manipulation for each route is sow to infer these). But its a bit harder to think of a different route tree structure for file based routing to be more efficient. Lots to think about My only concern about this is currently CheckPath. It checks if it is a valid route by path. While autocomplete will not suggest a branch for to, a branch will still be valid because the route exists... Basically it like a branch is valid only if from is specified and not to?
manual-pink
manual-pinkโ€ข2y ago
so we would need to different checks? or check paths should be parameterized?
ambitious-aqua
ambitious-aquaโ€ข2y ago
probably an additional check but i need to think so its not messy
ambitious-aqua
ambitious-aquaโ€ข2y ago
GitHub
fix: do not allow leaf nodes as to suggestions by chorobin ยท Pull...
Introduce RouteLeaves type to only autocomplete leaves We have change CheckPath aswell (yuck, I can't think of a better way) Add some type tests to cover only leaves
ambitious-aqua
ambitious-aquaโ€ข2y ago
Ignore the title on the image, I corrected it ๐Ÿ˜…
manual-pink
manual-pinkโ€ข2y ago
image? did you check the performance impact (if any)?
ambitious-aqua
ambitious-aquaโ€ข2y ago
yep, its a bit worse but I'm not sure how to not cause a slight regression because we have to distribute over the union to only get the leaves.
manual-pink
manual-pinkโ€ข2y ago
released as of v1.29.1
manual-pink
manual-pinkโ€ข2y ago
@TkDodo ๐Ÿ”ฎ I modified your example to show how these links are now not valid anymore: https://stackblitz.com/edit/tanstack-router-cvjex8?file=package.json,src%2Froutes%2F__root.tsx
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
manual-pink
manual-pinkโ€ข2y ago
No description
manual-pink
manual-pinkโ€ข2y ago
@Tanner Linsley the conditional redirect in route.tsx is a bit ugly... or maybe I am missing something. https://stackblitz.com/edit/tanstack-router-cvjex8?file=package.json,src%2Froutes%2Fworkspaces%2F%24id%2Froute.tsx,node_modules%2F%40tanstack%2Freact-router%2Fdist%2Fesm%2Froute.d.ts,src%2Fmain.tsx we don't have access to the "parsed" location in beforeLoad (by that I mean /workspaces/$id instead of /workspaces/c403d5c9-3a2d-4d57-a813-8fd5d2b6fb89), so this is what I came up with:
import { createFileRoute, redirect } from '@tanstack/react-router';
import { z } from 'zod';
export const Route = createFileRoute('/workspaces/$id')({
parseParams: (params) => ({
id: z.string().uuid().parse(params.id),
}),
beforeLoad: (opts) => {
if (opts.location.pathname.endsWith(`${opts.params.id}/`)) {
throw redirect({
to: '/workspaces/$id/help',
params: { id: opts.params.id },
});
}
},
});
import { createFileRoute, redirect } from '@tanstack/react-router';
import { z } from 'zod';
export const Route = createFileRoute('/workspaces/$id')({
parseParams: (params) => ({
id: z.string().uuid().parse(params.id),
}),
beforeLoad: (opts) => {
if (opts.location.pathname.endsWith(`${opts.params.id}/`)) {
throw redirect({
to: '/workspaces/$id/help',
params: { id: opts.params.id },
});
}
},
});
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
like-gold
like-goldOPโ€ข2y ago
Amazing, thank you
like-gold
like-goldOPโ€ข2y ago
@Manuel Schiller there's something odd now. If route.tsx renders a component, I will only see that component on help, while before, I would get both rendered. See here: https://stackblitz.com/edit/tanstack-router-w4r4dc?file=src%2Froutes%2Fworkspaces%2F%24id%2Froute.tsx,src%2Froutes%2Fworkspaces%2F%24id%2Fhelp.tsx If you navigate to help, you will only see "hello route.tsx" which comes fromroute.tsx. I would expect to see both "hello route.tsx" and "Hello /workspaces/$id/_layout/help!"
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
like-gold
like-goldOPโ€ข2y ago
at least that's how it worked before ๐Ÿ˜…
manual-pink
manual-pinkโ€ข2y ago
you need to add an <Outlet> to route.tsx otherwise the child matches will not be rendered see https://tanstack.com/router/latest/docs/framework/react/guide/outlets
like-gold
like-goldOPโ€ข2y ago
๐Ÿคฆโ€โ™‚๏ธ Thanks
ambitious-aqua
ambitious-aquaโ€ข2y ago
I'd like to fix trailing slashes aswell as I dont currently like the way it works. But I don't know if there is some nuance here with from. I guess with from there is an actual difference between a branch and an index route? Just had an idea. Can we safely say each routes fullPath always has a trailing slash if it is a leaf?
like-gold
like-goldOPโ€ข2y ago
not sure. we recently introduced that trailingSlashes are customizable on router level:
trailingSlash?: 'always' | 'never' | 'preserve';
trailingSlash?: 'always' | 'never' | 'preserve';
ambitious-aqua
ambitious-aquaโ€ข2y ago
We could apply this on the type level too probably. Like, always add a trailing slash to the leaf if always
like-gold
like-goldOPโ€ข2y ago
that would be great
ambitious-aqua
ambitious-aquaโ€ข2y ago
I will do a thread later about my thoughts on these three options and how we can support it on the type level as this might be the best way
deep-jade
deep-jadeโ€ข17mo ago
was this behaviour changed again? in the stackblitz link that @TkDodo ๐Ÿ”ฎ posted (https://stackblitz.com/edit/tanstack-router-w4r4dc?file=src%2Froutes%2Fworkspaces%2F%24id%2Froute.tsx,src%2Froutes%2Fworkspaces%2F%24id%2Fhelp.tsx) the code completion does not show the /workspaces/$id/ route. But with the most recent version, it's being visible in the list again. I just updated the dependency versions and the vite plugin according to the docs to get it working again. here is the updated code : https://stackblitz.com/edit/tanstack-router-dcmzoh?file=src%2Froutes%2Fworkspaces%2F%24id%2Froute.tsx
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
StackBlitz
Router Basic File Based Example (forked) - StackBlitz
Run official live example code for Router Basic File Based, created by Tanstack on StackBlitz
No description
deep-jade
deep-jadeโ€ข17mo ago
I stumbled upon this because I have a similar case where I'd like to have the following hierarchy: - /org - /org/$slug - /org/$slug/... and all routes under /org/$slug should share the same layout. So initially I thought I'd use a layout file like /org/_$slug but that didn't work and the I switched to /org/$slug/route.tsx with an Outlet, which works, but now org/$slug/ shows up as a potential link target. Are there any other reasons why one would use a underscore prefixed file as a layout than not adding that one to the list of potential paths? I can totally live with the fact that I have the link in my code completion if there is no real difference but code completion.
like-gold
like-goldOPโ€ข17mo ago
hmm @Chris Horobin I think in the shown sandbox, there shouldn't be /workspaces/$id/ available as a route to navigate to, because this route doesn't exist (there is no index route). so I'm on 1.45.4 and I'm also getting auto completion for links towards /workspaces/$id/ even though we only have a route.tsxthere (no on index route)
ambitious-aqua
ambitious-aquaโ€ข16mo ago
I believe this is correct. The types for to try to mimic the runtime behaviour. Provided you have trailing slashes set to always If you have an index route, org/$slug/ resolves to the index route as expected. If you have no index route, org/$slug/ resolves to route.tsx It's because it's not possible to navigate to route.tsx if a index route exists and trailing slashes are enforced
like-gold
like-goldOPโ€ข16mo ago
but imo it shouldn't be possible to navigate to a route.tsx route alone, right? I mean, route.tsx is used as a shared layout - it's not a route that "really exists"
ambitious-aqua
ambitious-aquaโ€ข16mo ago
Yeah, I think there were reasons for this @Manuel Schiller do you remember exactly the situation navigating to route.tsx is needed?
manual-pink
manual-pinkโ€ข16mo ago
hm I think it only makes sense in case no index exists
ambitious-aqua
ambitious-aquaโ€ข16mo ago
Yeah, which is what's currently happening, no? /org/$slug/ navigates to an index route if one exists, otherwise it navigates to the route.tsx? I think there was some reason to do with modals but I can't remember where the conversation was ๐Ÿค”
like-gold
like-goldOPโ€ข16mo ago
hm, the way I see it, route.tsx shouldn't exist for to (only for from), and if you want a real route to navigate towards, you should add an index.tsx route. But there could be some use-case I'm missing of course
deep-jade
deep-jadeโ€ข16mo ago
I just modified my routes in a way that instead of using a route.tsx I have the following structure with a "real" pathless route (I thought) routes/org/$slug routes/org/$slug/_apps.tsx <- layout, should not be possible to redirect to routes/org/$slug/_apps/app-a.tsx
routes/org/$slug/_apps/app-b.tsx But I still see /org/$slug in the to of a redirect, which doesn't make sense in terms of a pathless route, does it?
No description

Did you find this page helpful?