Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Monorepo example#755

Unanswered
tl-frameplay asked this question inQ&A
Oct 2, 2023· 6 comments· 13 replies
Discussion options

Hey guys, we appreciate you sharing this great library.

So far, everything is amazing, but we are having issues strongly type using a monorepo. Before we say which monorepo to avoid promoting one over the other; do you have any examples how tanstack router is meant to work for Typescript in a monorepo?

Specifically, we have a monorepo package called "router", where tanstack router is installed, and the actual "router" file in an app of the monorepo where all the routes are configured and the router exported for the app.

Thank you kindly.

image

You must be logged in to vote

Replies: 6 comments 13 replies

Comment options

With Nx Workspace, I updated the Vite Config as follows:

/// <reference types='vitest' />import{defineConfig}from'vite';importreactfrom'@vitejs/plugin-react';import{nxViteTsPaths}from'@nx/vite/plugins/nx-tsconfig-paths.plugin';import{TanStackRouterVite}from'@tanstack/router-vite-plugin';import{join}from'path';exportdefaultdefineConfig({root:__dirname,cacheDir:'../../node_modules/.vite/apps/web',server:{port:4200,host:'localhost',},preview:{port:4300,host:'localhost',},plugins:[react(),TanStackRouterVite({routesDirectory:join(__dirname,'src/routes'),generatedRouteTree:join(__dirname,'src/routeTree.gen.ts'),routeFileIgnorePrefix:'-',quoteStyle:'single',}),nxViteTsPaths(),],// Uncomment this if you are using workers.// worker: {//  plugins: [ nxViteTsPaths() ],// },build:{outDir:'../../dist/apps/web',reportCompressedSize:true,commonjsOptions:{transformMixedEsModules:true,},},test:{globals:true,cache:{dir:'../../node_modules/.vitest',},environment:'jsdom',include:['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],reporters:['default'],coverage:{reportsDirectory:'../../coverage/apps/web',provider:'v8',},},});
You must be logged in to vote
2 replies
@DNYLA
Comment options

I've set this up but was wondering how would i allow other libraries to share the types for my router so when using navigate() or Link i have full type saftey.

@bzbetty
Comment options

@rahulretnan do you find the watch still works? as soon as I add in a root: __dirname it regens fine on startup but seems to fail on changes.

Comment options

Hi@tl-frameplay@rahulretnan , can you please help me in figuring out how routing will work in Microfrontend architecture.. I am using turborepo and tanstack Router
I am just now sure, routing which are defined in packages will work in apps

You must be logged in to vote
2 replies
@mer10z
Comment options

I'm also using turborepo and trying to figure out how to do this. Currently we have a number of packages that are router agnostic and one app package that declares the router module. Now I'm trying to split up that app into multiple packages that all use the same router with type checking, but I'm not sure how to do it without creating a circular dependency between the packages. I've tried a few hacky things that didn't work and will continue trying, but if anyone has a solution to this it would be great to hear. We're not using file based routing so TanStackRouterVite isn't an option.

@davidturissini
Comment options

I'm starting to tackle this exact problem in our app. We are also using a monorepo setup. We have a top level "app" package that imports lots of other packages that are more focused on components, specific business logic, etc. We want the lower level library packages to be able to participate in Tanstack router's type safety. The problem is that we've defined all of the routes on the "app" level and the children packages cannot take a dependency on "app" because, well, circular dependencies and all the fun that comes with that.

But since we only care about making the types available to all children packages, I'm exploring adding a small typescript task that just outputs the route'sd.ts file into a lower level package that can be consumed by everyone. This seems to solve the circular dependency problem. This new "router" package does not contain any source code references back to the app. It's basically as if a developer manually copied thed.ts file and exported it.

Still working out how this plays out long term but its very promising so far.

Comment options

I guess,@tannerlinsley finally heard us.😅 He is including a example of tanstack router in monorepo. Thanks, Really, appreciate the Effort. ✨

Basic:
https://github.com/tanstack/router/tree/main/examples/react/router-monorepo-simple

With React Query:

https://github.com/TanStack/router/tree/main/examples/react/router-monorepo-react-query

You must be logged in to vote
1 reply
@schiller-manuel
Comment options

actually this was added by@beaussan :)

Comment options

I gave it a go and made a turborepo template with tanstack router at the core:

It's not a small example, with many other components such as

  • tailwind/shadcn
  • trpc
  • hono
  • drizzle-orm
  • better-auth

and so on, but hopefully someone finds it helpful :).

You must be logged in to vote
1 reply
@HipsterZipster
Comment options

this is awesome. nice job man!

Comment options

I'm going to hijack this discussion post to try to explain my main difficulty with the current recommendation of how Router supports monorepos, and what I think some potential solutions are. It all revolves around cyclical dependencies (get it?). What do I mean?

Lets say I have the monorepo setup that's currently in the docs:

.└── packages/    ├── app    ├── package-1    ├── package-2    └── router

The main thing to note here is thatrouter is where our routes folder,routeTree.gen.ts file, and router instance are defined. For the sake of this example, lets only worry about file based routing.app is really just an empty shell that consumes the routes. This setup looks great because now the exported functions fromrouter can now be consumed by the other packages to get that sweet, sweet, type inference. In theory too, if we had multiple apps / multiple routers, we'd be able to know which app/router is which because they export their own instance. Cool, looks good.

Now lets imagine that inrouter I have a route with some search params with some validation:

import { createFileRoute } from "@tanstack/react-router";import * as v from "valibot";const searchSchema = v.object({  redirect: v.optional(v.string()),});export const Route = createFileRoute("/sign-in")({  component: SignInPage,  validateSearch: searchSchema,});function SignInPage() {  const { redirect } = Route.useSearch();  return <>Hello</>;}

Now lets add a component frompackage-1 to the route:

import { Redirect } from "@acme/package-1"import { createFileRoute } from "@tanstack/react-router";import * as v from "valibot";const searchSchema = v.object({  redirect: v.optional(v.string()),});export const Route = createFileRoute("/sign-in")({  component: SignInPage,  validateSearch: searchSchema,});function SignInPage() {  const { redirect } = Route.useSearch();  return (    <>      <Redirect />    </>  );}

Lets say that this this component is the actual thing that cares about our search param. These typed & validated search params are a really powerful feature of Router. Personally, I really like the idea of having component that require / consume these params be responsible for getting them, similar to other ideas of having components that consume specific data be responsible for getting that data. Router thought of that too, and it's great that you can access to these typed & validated values even outside of route components via theRoute object or thegetRouteApi function.

Except wait a second, how doespackage-1 access those types or objects? Well it would need to install ourrouter package as a workspace dependency, consume those typed APIs, and then export our component again to be consumed byrouter:

import { getRouteApi } from "@acme/router";const routeApi = getRouteApi('/sign-in');export const Redirect = () => {  const routeSearch = routeApi.useSearch();  return <>Do something with redirect {routeSearch.redirect}</>;}

But now we've created a cyclical dependency:

router -> package-1 -> router -> app

Big no-no in monorepos, most tools will yell at you if you have cyclical dependencies, if notoutright refuse to run.

So how to solve this? Well there's a few ideas I can think of:

1). Router concerns are router concerns.

Leave the router stuff to the router and have packages expect that data via props / another data contract:

function SignInPage() {  const { redirect } = Route.useSearch();  return (    <>      <Redirect redirect={redirect} />    </>  );}

I'm totally cool with this being the answer. It's not what Iwant, but it makes sense. Now our graph has the correct direction again:package-1 -> router -> app. But we lose that composability that can be really powerful with component based design. I can't have the component that actually cares about & uses the data be responsible for getting it .

2). Loosen types

Loosen up our types as outlined in the docs:

const search = useSearch({    strict: false,  })

I'm down to loosen these types, and type them inside our package instead. But we're better than that, right? Ok cool, moving on.

3). Create a new sink for the Route APIs in our graph

Go onto any of these GitHub issues where people complain about cyclical dependencies and the first answer is to create a new package or combine existing packages. We're not gonna combinepackage-1 androuter, the whole point of the monorepo is to split them up, so lets focus on the former. Our graph would become something like

package-that-has-router-apis -> package-1 && router -> app

But Router doesn't really support this at the moment. When I generate my types for therouter, they're kind of stuck inside thatrouter package. Maybe this is the reasoning behind the idea of "Move all generated code out of the src and app directories. Preferably a .tanstack directory." in theV2 ideas. This opens up a can of worms if you have multiple router instances in the same repo, so I can see why this might be tricky. You might have to have separate sink packages for each router, idk.

4). Import the package in the app only

This is how the current monorepo example is set up. In this paradigm,package-1 would export the page, not found, and error components for that route, then we set those components on that route inapp. This is the real answer, right? I'm not convinced. To me--and this is just pure personal preference--theapp is the router, and the router is the app. I don't really like the idea of splitting up mypage from myrouter, I think the DX of having my router far away from the page / layout / outlet that's rendered is kind of backwards.

With file based routing, I really like how we can look at our files in our routes folder and understand the structure of the app at a glance, AND seeing the actual page rendered on the route. This colocation of pages and routes is really powerful and is a big reason for me why these file based routes are so powerful. I think by spreading them out like this we've taken the idea of the monorepo too far. I want my router to be responsible for pages and routing, not just routing. Maybe this means that we do need to go back to the single responsibility pattern outlined in solution 1, I'm not sure. All of this is to say that something about splitting my pages from my router feels off to me. I'm open to being wrong on this, but I feel like I'm losing a key advantage of what I like about file based routing.

5). Cheat

This is kind of what@davidturissini mentioned in their reply above, which is to just grab thetypes via a script or task and make them available at a lower package in the graph. I'd go even further honestly, I'd be fine with putting something like that at the root of the repo, something like arouterGen.d.ts at the root of the repo that only overwrites types. A package is probably better, but the point is to get these types available somewhere lower in the graph / ambiently available in the repo. This has the same issue as above for if there are multiple routers in the same repo, but probably alleviated by good package management.

My preferred solution
I would love for Router to be able to generate these types for me, but not necessarily tie them to myrouter orapp package. The type generation & type safety is probably my favorite feature of Router, but it's difficult to leverage them in a monorepo. I would love to be able to tell Router "Generate X files in Y directories, I'll handle hooking everything up so they're imported and used correctly.". If there's a way to do that now, I haven't really been able to get it to work correctly by changing thegeneratedRouteTree path option.

Someone tell me if I'm missing something or doing something wrong, because I've found it difficult to get my sweet, sweet type safety with Router in a monorepo. Idk, let me know what you think or if I'm over complicating this.

You must be logged in to vote
7 replies
@alexjball
Comment options

Thank you@taylorfsteele for this suggestion! I added a step to the types package that bundles the app types and then imports that. This avoids having to type check the main app from each feature package, and allows the main app to have a different tsconfig/path aliases.

import{defineConfig}from"tsdown";exportdefaultdefineConfig((options)=>({dts:{emitDtsOnly:true,},clean:!options.watch,entry:["../../apps/web/src/router.tsx"],outDir:"../../apps/web/dist-types",tsconfig:"../../apps/web/tsconfig.app.json",}));
// packages/web-router/src/index.ts// gets compiled by tsc and replicates the standard router re-export from the monorepo exampleimport{useRouteContext}from"@tanstack/react-router";importtype{RouterIds,RouterType,SaasRouterContext}from"../../../apps/web/dist-types/router";exporttype{RouterIds,RouterType,SaasRouterContext};exportconstuseSaasRouterContext=()=>{constcontext:SaasRouterContext=useRouteContext({from:"__root__"});returncontext;};exporttype{ErrorComponentProps,RegisteredRouter,RouteById}from"@tanstack/react-router";// By re exporting the api from TanStack router, we can enforce that other packages// rely on this one instead, making the type register being appliedexport{createLazyRoute,ErrorComponent,getRouteApi,Link,Outlet,RouterProvider,useLocation,useNavigate,useParams,useRouteContext,useRouter,useSearch,}from"@tanstack/react-router";

Build withtsdown && rm -rf dist && tsc then consume from features like

import{getRouteApi}from'@acme/web-router';

Caching builds is a little weird because the router types can depend on feature packages. but it is much easier to build out features without having to split them between the router and app sides. It also makes it feasible to add Tanstack Start/automatic code splitting, which I haven't figured out how to support in the existing monorepo pattern.

@taylorfsteele
Comment options

I've been stewing on this for a while, and the more I think about it, the more I think I prefer your solution,@alexjball. I think the thing for me is that as the project grows, and the types become a bit more involved, I really don't like how my solution causes cyclical typechecks (e.g., when I runtsc --noEmit in "@acme/auth" it also checks the types in "@acme/web"). The more I think about it, the more a dedicated build step to create these types might be the way to do. I don't want to sacrifice the speed of my typechecking for the sake of running a build. I might end tacking this onto the build & dev commands for the web app where my router is, just so it generates these types alongside the other code generated by Router. If I can figure out how to skip this step / use cached builds if nothing has previously changed, even better.

More to the point of this whole discussion however, this might be where TanStack Router can help. Router already does some amount of codegen types viarouteTree.gen.ts, and I could imagine that a future option to Router that helps to create these files (the files that import the routes fromrouter/.tsx and re-export the@tanstack/router APIs for use in other packages).

I'll fiddle with this, but I do think that generating these types in a single build step is better than my solution of slowing down typechecking throughout the project. I'll report back if I find anything to help with caching & maybe improving the DX.

@taylorfsteele
Comment options

Finally got around to fixing this after I added a new package and my typecheck step took twice as long 😓

I went ahead with@alexjball 's approach and used a build to reach across package boundaries to get the right types. I don't quite see the valueyet in exporting the actual@tanstack/router APIs from a router package like above & in the monorepo example, so I'm sticking to only a types approach for now. Could change, but I'm mostly focused on types for now.

// configs/types/tsdown.config.tsimport { defineConfig } from "tsdown";export default defineConfig((options) => ({  dts: {    emitDtsOnly: true,  },  clean: !options.watch,  entry: ["../../apps/web/src/router.tsx"],  outDir: "dist-types",  tsconfig: "./tsconfig.json",}));

I ran into errors when trying to correctly build out the router, the build step wasn't getting the types for myRootContext correctly:

// apps/web/src/routes/__root.tsximport type { Subscriptions, User } from "@acme/auth";type RootContext = {  queryClient: QueryClient;  user: User | null;  subscriptions: Subscriptions[] | null;};export const Route = createRootRouteWithContext<RootContext>()({})

In my case I'm not using any path aliasing (I find they're too annoying inside monorepos), so I had to tell tsdown where to find these correctly. Your milage may vary.

// configs/types/tsconfig.json{  "extends": "@acme/typescript-config/react.json",  "include": ["."],  "exclude": ["node_modules", "dist"],  "compilerOptions": {    "types": ["vite/client"],    "baseUrl": ".",    "paths": {      "@acme/auth": ["../../packages/auth/src/index.ts"]    }  }}

The last step was making sure that my types package had access to@tanstack/router-core. I'm using pnpmcatalogs to manage the package versions across the monorepo:

// configs/types/package.json{  "name": "@acme/types",  "type": "module",  "types": "./index.d.ts",  "scripts": {    "build-types": "tsdown"  },  "devDependencies": {    "@tanstack/react-query": "catalog:",    "@tanstack/react-router": "catalog:@tanstack/react-router",    "@tanstack/react-start": "catalog:@tanstack/react-router",    "@tanstack/router-core": "catalog:@tanstack/react-router", // <--- Need this to register types correctly    "tsdown": "^0.15.1",    "typescript": "catalog:",    "vite": "catalog:"  }}

I'm consuming it just like before, where my other packages install this types package as a dev dependency and reference it inside their tsconfig:

  "compilerOptions": {    "types": ["@acme/types"]  }

Types performance is way better now, at the tradeoff of having to run a build task in order to get the types correct. I think that's a fine tradeoff, your favorite monorepo tools can help you manage that build step.

@ifxnas
Comment options

@taylorfsteele Hi Taylor, I've been following this discussion for some time, and in pretty much similar boat. Would really appreciate a poc in action just so we can experiment. Thanks! 🙌

@taylorfsteele
Comment options

@ifxnas Did you ever get this working? I can try to put up a full repo as an example to help, but it really just would be the steps I outlined above.

I'm still using tsdown & built types solution and it's... fine. I'm using Turborepo as my monorepo tool, and it's pretty easy to set up a graph that runs mybuild-types task before checking types:

{  "tasks": {    "build-types": {      "outputs": ["dist-types/"]    },    "typecheck": {      "dependsOn": ["^build-types"]    }  }}

Type performance still feels a little sluggish to me, but I think that has more to do with the multiple layers of inference in the repo. The giantrouter.d.ts file that this generates probably doesn't help.

Comment options

Hey everyone,
I’m still trying to figure out the best way to set up my monorepo with TanStack Router while keeping type autocompletion working.

My structure looks like this:

.└── apps/    ├── app-1    └── app-2└── packages/    ├── router    ├── shared    └── ...

The main thing I’m aiming for is thatapp-1 andapp-2 share a bunch of identical routes (same paths and content), which I’d like to keep inshared. Then each app would add its own extra routes on top of those. Ideally, autocompletion would only show the shared routes inside shared, and then app-specific routes would autocomplete only in their respective apps.

Has anyone found a good way to do this with the current state of TanStack Router?

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
Q&A
Labels
None yet
14 participants
@tl-frameplay@HipsterZipster@bzbetty@davidturissini@mer10z@schiller-manuel@alexjball@DNYLA@myeljoud@rahulretnan@sohamnandi77@taylorfsteele@nktnet1@ifxnas

[8]ページ先頭

©2009-2025 Movatter.jp