T
TanStack2y ago
correct-apricot

monorepo setup

what's the recommended setup for working in a monorepo, given that TanStack Router uses module augmentation? we have a monorepo with a composite setup, so each package emits its own .d.ts files. Now our setup is roughly like this:
- apps
- app-one
- routes
- libs
- some-shared-lib
- apps
- app-one
- routes
- libs
- some-shared-lib
now app-one has the routes, the generated route-tree and the module augmentation of @tanstack/react-router. but inside libs/some-shared-lib, I would like to have a component that renders a <Link> from the router. As far as I can see, the module augmentation is not "seen" inside that lib, so I don't have type-safety for anything there. How could we solve this?
41 Replies
correct-apricot
correct-apricotOP2y ago
would it maybe work to have the generation output multiple routeTree.gen.ts files - one per package ?
flat-fuchsia
flat-fuchsia2y ago
We can discuss more here. Apart from the cyclic dependency that only imports the type. You could use paths. Maybe that will help because only TS knows about the dependency, not your bundler. I was thinking about a separate package like packages/routes where you would run the generator and both the app and shared lib would import from it. You would need to fill in the component and loader with update in app. But I'm not sure if I like this solution either because you might end up pushing all your code into packages/routes in the end and I don't think it will pick up the loader types actually Or we need some new concept in router. Like an abstract router but this might be beyond my API design skills
absent-sapphire
absent-sapphire2y ago
I found the following relevant SO threads: one uses pathsals @Chris Horobin suggested: https://stackoverflow.com/a/75699722/678093
Stack Overflow
Material UI module augmentation across multiple packages in a monorepo
I am using lerna to create multiple UI packages. I am augmenting @material-ui/styles in package a to add more palette and typography definitions. I am able to get the new types in package a. Packag...
absent-sapphire
absent-sapphire2y ago
the other one imports the file that contains the declare module in the other packages: https://stackoverflow.com/a/69907717/678093
Stack Overflow
Share typescript module with every package in a monorepo
I have a monorepo of react components that are built using styled-components, getting their themes from a theme provider. declare module 'styled-components' { export interface DefaultTheme {
correct-apricot
correct-apricotOP2y ago
I found both the answers on SO too, but they weren't really helpful :/ I also wanted to explore the separate package, but couldn't get it to work. The routeTree.gen also does module augmentation and it imports all the routes, so the shared package needs to know about all the route files, and likely even create the router instance itself and export it
absent-sapphire
absent-sapphire2y ago
why did both not work for you? can you share some more insights here? can this be solved by generating different output in the router generator? if yes, what would you need?
correct-apricot
correct-apricotOP2y ago
A coworker mentioned yesterday that paths can work and we'll talk about it today. If we get this done I'd like to add a file-based monorepo example to the docs
inland-turquoise
inland-turquoise2y ago
That would be awesome. I had previously tinkered with using file-based routing in both the root app and the underlying lib, which "worked" (rendered the right components with correct nesting) except I wasn't able to figure out how to attach the route tree in the library to a branch of the route tree in the root, which meant the current URL was never updated, behavior was a bit odd, and there were type errors since the root app wasn't aware of the sub app routes. Being able to import and attach route trees to existing route trees to extend them send like it should be possible, but I wasn't able to get the types to line up.
useful-bronze
useful-bronze2y ago
Did anyone ever figure out a viable, type-safe monorepo solution for TSR?
correct-apricot
correct-apricotOP2y ago
Yes, I did 🙂 will try to write about it soon
correct-apricot
correct-apricot13mo ago
@TkDodo 🔮 Any progress on this? Or a working solution/example that you could share? We're facing the same issue and can't figure it out properly.
correct-apricot
correct-apricotOP13mo ago
Yes, there's an open source repro by @Nicolas Beaussart
correct-apricot
correct-apricotOP13mo ago
GitHub
GitHub - beaussan/tanstack-router-react-mono
Contribute to beaussan/tanstack-router-react-mono development by creating an account on GitHub.
correct-apricot
correct-apricot13mo ago
Thank you for sharing 🙏 I wanted to avoid this solution because of the giant routeMap - we have ~1000 routes so I don't want to import all the components and loaders in 1 place. You also lose the ability to click-through from the route definition directly to the component. Another option would be for each domain/feature to register its own components 🤔 I'm just wondering if it's somehow possible for the route package to depend on the feature packages and provide the types back to them without circular dependencies... Could virtual routes help here or it doesn't change anything type-wise?
absent-sapphire
absent-sapphire13mo ago
TanStack | High Quality Open-Source Software for Web Developers
Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components.
From An unknown user
From An unknown user
correct-apricot
correct-apricot13mo ago
@Manuel Schiller I saw that one as well, but it's the same solution - define a skeleton router and dynamically inject all components etc, which doesn't really scale for hundreds of routes 🫤
correct-apricot
correct-apricotOP13mo ago
@honzk do you have a better suggestion on how to avoid the circularity? I mean if you expect to have routes that render components where the components depend on the route definition, and you have that in different packages, that is a circularity you have to break. my suggestion is to break it up between router and component: to create a router, with everything it needs to infer the types (paths, loaders, search param schemas, ...) but without components, then later go into your app (that needs to depend on everything anyways) and add the components, likely with lazy loading.
we have ~1000 routes so I don't want to import all the components and loaders in 1 place
why loaders? only components need to be imported imo. And right now, this is the best trade-off I have to offer. If you know something better, I'd be happy to hear it 🙂
correct-apricot
correct-apricot13mo ago
If you want to trade flexibility for less type safety, you could imagine a plugin like architecture where you define the connected component to a bunch of route, defined in a plugin close to the feature libraries, and then from the list of plugins update the router, however this has a huge drawback: you could have a route without components, because you don't enforce route = component, and you will have no way of knowing it
correct-apricot
correct-apricot13mo ago
Loaders might contain domain-specific logic that lives next to the components. Even without considering loaders, this becomes quite messy. That being said, I don’t have any better solution, I’m just trying to figure out the best way to do this in scale.
harsh-harlequin
harsh-harlequin13mo ago
We have a similar monorepo setup. Currently handling this by adding a “export routes” command to the package that contains the router. “Export routes” makes a type only build and outputs the types to the root of the monorepo. Then individual packages that need this add the root file to their includes config Every package that includes the outputted types can just import tanstack router and have it work as expected.
jolly-crimson
jolly-crimson11mo ago
@DavidDavid Do you mind sharing this setup with us? @Nicolas Beaussart I had a look at your example repo, thank you for taking the time to create it! While i appreciate the effort, I think the setup introduces a lot of complexity, basically to please Typescript and have type-safe routes. Do you think this added complexity is a worthwhile trade-off? I’ve been wrestling with this problem for the past few days but haven’t been able to find an alternative solution. I have the following setup:
- apps
- app-one
- routes (all routes are configured here + Vite Plugin which points to tree gen file in router lib)
- dashboard.tsx (this route imports dashboard lib)
- libs
- dashboard (imports router lib)
- router (re-exports tanstack router and has module augmentation)
- apps
- app-one
- routes (all routes are configured here + Vite Plugin which points to tree gen file in router lib)
- dashboard.tsx (this route imports dashboard lib)
- libs
- dashboard (imports router lib)
- router (re-exports tanstack router and has module augmentation)
I created the router library purely to have type-safety and to export a custom Link component.
correct-apricot
correct-apricot11mo ago
This is like the other example I've contributed to the TanStack router docs https://tanstack.com/router/latest/docs/framework/react/examples/router-monorepo-simple Basically, this works as it is, however if you need to share query options between the router and the dashboard lib in your example, that's where the data access kind of library make sense, or if you only use loader that works great To answer the question above "do you think it's worth the trade off?" it depends on the size : If you have few teams contributing to the monorepo, maybe not, and maybe that's fine for you to expose query options directly from the router lib, but if you start having 100+ contrib on the app / repo, then having the data access library make sense in terms of boundaries and contribution model, because at the end, a monorepo is a solution to a organisational issue
React TanStack Router Router Monorepo Simple Example | TanStack Rou...
An example showing how to implement Router Monorepo Simple in React using TanStack Router.
jolly-crimson
jolly-crimson11mo ago
I was actually more referring to the decision of having the routing definitions in one lib and the route mapping in the application, doesn't feel intuitive imo. So I was mostly questioning that trade-off, also you loose the ability of route splitting with this setup. You can ofc lazy load the components but things like pre-fetching when hovering a link don't work anymore. The whole module augmentation thing has a huge impact on how we have to structure our mono-repo and I find that very limiting. Is this a TS limitation or TSR limitation?
correct-apricot
correct-apricot11mo ago
Maybe there is something we can do on the pre fetching of components on link hover when we use manually lazy in the monorepo route to component @Manuel Schiller ?
absent-sapphire
absent-sapphire11mo ago
does it not work automatically?
correct-apricot
correct-apricot11mo ago
It seems like no from @jsef comments I'll make an docs example with lazys and we shall see! I'll let you know, and it it dosn't work we can start from there
jolly-crimson
jolly-crimson11mo ago
Well, it might be that I didn't configure it properly. This is what I did:
const MyComponent = React.lazy(() => import('./MyComponent'));

const routesMap = {
"about": () => <Suspense placeholder="Loading..."><MyComponent /></<Suspense>
}
const MyComponent = React.lazy(() => import('./MyComponent'));

const routesMap = {
"about": () => <Suspense placeholder="Loading..."><MyComponent /></<Suspense>
}
Do I have to use the .lazy suffix in the file name of the route and use createLazyFileRoute?
correct-apricot
correct-apricot11mo ago
Hey, I've riff off a few ideas, and this is what I cam up with: https://github.com/TanStack/router/pull/3244 Feature libs expots a lazy route (createLazyRoute), that is then linked to the actual route on the end app, this supports lazy loading of pages with also pre loading with mouse hover intent Cc @TkDodo 🔮 , I'm sure this will peak your interest, no more route.update in this case
GitHub
docs: add lazy loaded monorepo example by beaussan · Pull Request #...
Key files to look at: Where we plug the lazy routes with the routes Example exposed route from a feature library Re exported types and functions from the router to have type augmentation
correct-apricot
correct-apricotOP11mo ago
looks good, but is conceptually the same, no? We're just trading a call to route.update for route.lazy(component), and we also need the manual routerMap
correct-apricot
correct-apricot11mo ago
Nearly, one thing differs: with route.lazy() the component is pre loaded on link hover vs after click, plus it allow to colocate not foud, error, pending as one config
correct-apricot
correct-apricotOP11mo ago
oooh, yeah that's nice
correct-apricot
correct-apricot8mo ago
Hi @honzk , I am trying to setup tanstack router in a monorepo and I have same requirements as you. Have you found some viable solution?
deep-jade
deep-jade8mo ago
If I'm understanding correctly, to use with a monorepo setup we have to manually add each route to the routermap?
const routerMap = {
'/': PostsListComponent,
'/$postId': PostIdComponent,
__root__: RootComponent,
} as const satisfies Record<RouterIds, () => React.ReactElement>

Object.entries(routerMap).forEach(([path, component]) => {
const foundRoute = router.routesById[path as RouterIds]

foundRoute.update({
component: component,
})
})
const routerMap = {
'/': PostsListComponent,
'/$postId': PostIdComponent,
__root__: RootComponent,
} as const satisfies Record<RouterIds, () => React.ReactElement>

Object.entries(routerMap).forEach(([path, component]) => {
const foundRoute = router.routesById[path as RouterIds]

foundRoute.update({
component: component,
})
})
correct-apricot
correct-apricot7mo ago
Yes, that's the recommended way. I really like TanStack Router, but this monorepo setup isn't good from the developer experience standpoint. I hope there will be a better way in the future
correct-apricot
correct-apricot7mo ago
I have a experiment with Nx sync generator to keep this map up to date based on lazy route exposed by featue libraries https://github.com/beaussan/tanstack-router-sync-experiment I need to test it on a larger scale repo, but that removed one part of the setup (based on https://nx.dev/concepts/sync-generators )
GitHub
GitHub - beaussan/tanstack-router-sync-experiment
Contribute to beaussan/tanstack-router-sync-experiment development by creating an account on GitHub.
Nx
Sync Generators
Learn how to use Nx sync generators to maintain repository state and update configuration files based on the project graph before tasks are run.
extended-salmon
extended-salmon7mo ago
Hi im trying to setup a monorepo with tanstack/router and nestjs as backend as well. The only thing im actually interested in is to have the routes shared between frontend and backend. I thought about this setup
- apps
- frontend
- routes
- backend
- libs
- routes/routeTree
- apps
- frontend
- routes
- backend
- libs
- routes/routeTree
basically the frontend generates and consumes the routeTree from libs/routes and the backend can consume the routes just to have route autocompletion. This way we can create emails/notifications etc in the backend on existing routes and we don't need to guess them. How can we implement this?
jolly-crimson
jolly-crimson6mo ago
Hey @Nicolas Beaussart , isn't this a bit overengineered considering the actual benefits of it? I got used to updating the map manually since it is strongly typed and would get a type error anyway when building the app.
correct-apricot
correct-apricot6mo ago
I don't have an off the shelf solution yet, just a poc that is public For a small team maybe, but I'm working with a team of 150+ eng on the same app, so automation like that are kinda needed 😅
jolly-crimson
jolly-crimson6mo ago
Oh yeah, that makes sense :p I assume you are using NX?
correct-apricot
correct-apricot6mo ago
Yup, heavily! Couldn't run the frontend platform without it
stormy-gold
stormy-gold6mo ago
no. never. if you think it's overengineering to use nx, then don't do a monorepo. the moment you go monorepo, there are so many things you need to nail down and put gaurdrails around. there's a difference between: - yes my naive approach works because it doesn't explode - i want to scale onboarding other team members and don't want to spend my life doing PR reviews on stuff that could have been systemised and automated

Did you find this page helpful?