Skip to content

Commit

Permalink
perf: new addChildren signature and generate more (#1564)
Browse files Browse the repository at this point in the history
Be sure to re-run generator after upgrading

* perf: get lazy? do less?, generate more

* feat: add support for objects for children at runtime

- add type tests for objects with children
- add runtime support for objects with children
- add runtime tests for objects with children

* perf: apply similar optimisations for merging for code based

* perf: improve performance of LinkProps when passed to `Link`

* exmaples: add to large based file examples search, params and context

* docs: add TS performance recommendations to docs

* fix: remove route groups from id

* examples: update linkprops examples

* docs: update docs based on comments

* chore: remove export of `LinkComponentProps`

* chore: clean up PR

---------

Co-authored-by: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com>
  • Loading branch information
chorobin and lachlancollins committed May 13, 2024
1 parent 89907c6 commit 0898f2e
Show file tree
Hide file tree
Showing 29 changed files with 1,676 additions and 568 deletions.
137 changes: 137 additions & 0 deletions docs/framework/react/guide/type-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,140 @@ const router = createRouter({
},
})
```

## Performance Recommendations

As your application scales, TypeScript check times will naturally increase. There are a few things to keep in mind when your application scales to keep your TS check times down.

### Narrow to relevant routes as much as you possibly can

Consider the following usage of `Link`

```tsx
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
<Link search={{ page: 0 }} />
```

**These examples are bad for TS performance**. That's because `search` resolves to a union of all `search` params for all routes and TS has to check whatever you pass to the `search` prop against this potentially big union. As your application grows, this check time will increase linearly to number of routes and search params. We have done our best to optimise for this case (TypeScript will typically do this work once and cache it) but the initial check against this large union is expensive. This also applies to `params` and other API's such as `useSearch`, `useParams`, `useNavigate` etc

Instead you should try to narrow to relevant routes with `from` or `to`

```tsx
<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />
```

Remember you can always pass a union to `to` or `from` to narrow the routes you're interested in.

```tsx
const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />
```

You can also pass branches to `from` to only resolve `search` or `params` to be from any descendants of that branch

```tsx
const from = '/posts'
<Link from={from} to='..' />
```

`/posts` could be a branch with many descendants which share the same `search` or `params`

### Consider using the object syntax of `addChildren`

It's tyipcal of routes to have `params` `search`, `loaders` or `context` that can even reference external dependencies which are also heavy on TS inference. For such applications, using objects for creating the route tree can be more performant than tuples.

`createChildren` also can accept an object. For large route trees with complex routes and external libraries, objects can be much faster for TS to type check as opposed to large tuples. The performance gains depend on your project, what external dependencies you have and how the types for those libraries are written

```tsx
const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
})
```

Note this syntax is more verbose but has better TS performance. With file based routing, the route tree is generated for you so a verbose route tree is not a concern

### Avoid internal types without narrowing

It's common you might want to re-use types exposed. For example you might be tempted to use `LinkProps` like so

```tsx
const props: LinkProps = {
to: '/posts/',
}

return (
<Link {...props}>
)
```

**This is VERY bad for TS Performance**. The problem here is `LinkProps` has no type arguments and is therefore an extremely large type. It includes `search` which is a union of all `search` params, it contains `params` which is a union of all `params`. When merging this object with `Link` it will do a structural comparison of this huge type.

Instead you can use `as const satisfies` to infer a precise type and not `LinkProps` directly to avoid the huge check

```tsx
const props = {
to: '/posts/',
} as const satisfies LinkProps

return (
<Link {...props}>
)
```

As `props` is not of type `LinkProps` and therefore this check is cheaper because the type is much more precise. You can also improve type checking further by narrowing `LinkProps`

```tsx
const props = {
to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
<Link {...props}>
)
```

This is even faster as we're checking against the narrowed `LinkProps` type.

You can also use this to narrow the type of `LinkProps` to a specific type to be used as a prop or parameter to a function

```tsx
export const myLinkProps = [
{
to: '/posts',
},
{
to: '/posts/$postId',
params: { postId: 'postId' },
},
] as const satisfies ReadonlyArray<LinkProps>

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
return <Link {...props.linkProps} />
}
```

This is faster than using `LinkProps` directly in a component because `MyLinkProps` is a much more precise type

Another solution is not to use `LinkProps` and to provide inversion of control to render a `Link` component narrowed to a specific route. Render props are a good method of inverting control to the user of a component

```tsx
export interface MyComponentProps {
readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
return <div>{props.renderLink()}</div>
}

const Page = () => {
return <MyComponent renderLink={() => <Link to="/absolute" />} />
}
```

This particular example is very fast as we've inverted control of where we're navigating to the user of the component. The `Link` is narrowed to the exact route
we want to navigate to
41 changes: 34 additions & 7 deletions examples/react/basic-file-based/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,38 +73,65 @@ const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/_layout': {
id: '/_layout'
path: ''
fullPath: ''
preLoaderRoute: typeof LayoutImport
parentRoute: typeof rootRoute
}
'/posts': {
id: '/posts'
path: '/posts'
fullPath: '/posts'
preLoaderRoute: typeof PostsImport
parentRoute: typeof rootRoute
}
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
fullPath: ''
preLoaderRoute: typeof LayoutLayout2Import
parentRoute: typeof LayoutImport
}
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
fullPath: '/posts/$postId'
preLoaderRoute: typeof PostsPostIdImport
parentRoute: typeof PostsImport
}
'/posts/': {
id: '/posts/'
path: '/'
fullPath: '/posts/'
preLoaderRoute: typeof PostsIndexImport
parentRoute: typeof PostsImport
}
'/_layout/_layout-2/layout-a': {
id: '/_layout/_layout-2/layout-a'
path: '/layout-a'
fullPath: '/layout-a'
preLoaderRoute: typeof LayoutLayout2LayoutAImport
parentRoute: typeof LayoutLayout2Import
}
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
fullPath: '/layout-b'
preLoaderRoute: typeof LayoutLayout2LayoutBImport
parentRoute: typeof LayoutLayout2Import
}
'/posts/$postId/deep': {
id: '/posts/$postId/deep'
path: '/posts/$postId/deep'
fullPath: '/posts/$postId/deep'
preLoaderRoute: typeof PostsPostIdDeepImport
parentRoute: typeof rootRoute
}
Expand All @@ -113,16 +140,16 @@ declare module '@tanstack/react-router' {

// Create and export the route tree

export const routeTree = rootRoute.addChildren([
export const routeTree = rootRoute.addChildren({
IndexRoute,
LayoutRoute.addChildren([
LayoutLayout2Route.addChildren([
LayoutRoute: LayoutRoute.addChildren({
LayoutLayout2Route: LayoutLayout2Route.addChildren({
LayoutLayout2LayoutARoute,
LayoutLayout2LayoutBRoute,
]),
]),
PostsRoute.addChildren([PostsPostIdRoute, PostsIndexRoute]),
}),
}),
PostsRoute: PostsRoute.addChildren({ PostsPostIdRoute, PostsIndexRoute }),
PostsPostIdDeepRoute,
])
})

/* prettier-ignore-end */
1 change: 1 addition & 0 deletions examples/react/large-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"test:types": "tsc --extendedDiagnostics"
},
"dependencies": {
"@tanstack/react-query": "^5.17.8",
"@tanstack/react-router": "^1.32.0",
"@tanstack/router-devtools": "^1.32.0",
"@tanstack/router-vite-plugin": "^1.31.18",
Expand Down
29 changes: 28 additions & 1 deletion examples/react/large-file-based/src/createRoutes.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import { readFile, writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'

const length = 200
const length = 100

const main = async () => {
const absolute = (await readFile('./src/routes/absolute.tsx')).toString()
const relative = (await readFile('./src/routes/relative.tsx')).toString()
const searchRoute = (
await readFile('./src/routes/search/route.tsx')
).toString()
const search = (
await readFile('./src/routes/search/searchPlaceholder.tsx')
).toString()
const paramsRoute = (
await readFile('./src/routes/params/route.tsx')
).toString()
const params = await (
await readFile('./src/routes/params/$paramsPlaceholder.tsx')
).toString()

if (!existsSync('./src/routes/(gen)')) {
await mkdir('./src/routes/(gen)')
}

if (!existsSync('./src/routes/(gen)/search')) {
await mkdir('./src/routes/(gen)/search')
}

if (!existsSync('./src/routes/(gen)/params')) {
await mkdir('./src/routes/(gen)/params')
}

await writeFile('./src/routes/(gen)/search/route.tsx', searchRoute)
await writeFile('./src/routes/(gen)/params/route.tsx', paramsRoute)

for (let y = 0; y < length; y = y + 1) {
const replacedAbsolute = absolute.replaceAll('/absolute', `/absolute${y}`)
const replacedRelative = relative.replaceAll('/relative', `/relative${y}`)
const replacedSearch = search.replaceAll('searchPlaceholder', `search${y}`)
const replacedParams = params.replaceAll('paramsPlaceholder', `param${y}`)
await writeFile(`./src/routes/(gen)/absolute${y}.tsx`, replacedAbsolute)
await writeFile(`./src/routes/(gen)/relative${y}.tsx`, replacedRelative)
await writeFile(`./src/routes/(gen)/search/search${y}.tsx`, replacedSearch)
await writeFile(`./src/routes/(gen)/params/$param${y}.tsx`, replacedParams)
}
}

Expand Down
6 changes: 6 additions & 0 deletions examples/react/large-file-based/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { routeTree } from './routeTree.gen'

export const queryClient = new QueryClient()

// Set up a Router instance
const router = createRouter({
routeTree,
defaultPreload: 'intent',
context: {
queryClient,
},
})

// Register things for typesafety
Expand Down

0 comments on commit 0898f2e

Please sign in to comment.