Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/router/RouteOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The `RouteOptions` type is used to describe the options that can be used when cr
- Type: `(rawSearchParams: unknown) => TSearchSchema`
- Optional
- A function that will be called when this route is matched and passed the raw search params from the current location and return valid parsed search params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's search params and the return type will be inferred into the rest of the router.
- Optionally, the parameter type can be tagged with the `SearchSchemaInput` type like this: `(searchParams: TSearchSchemaInput & SearchSchemaInput) => TSearchSchema`. If this tag is present, `TSearchSchemaInput` will be used to type the `search` property of `<Link />` and `navigate()` **instead of** `TSearchSchema`. The difference between `TSearchSchemaInput` and `TSearchSchema` can be useful, for example, to express optional search parameters.

#### `parseParams`

Expand Down
2 changes: 1 addition & 1 deletion docs/api/router/SearchParamOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The `SearchParamOptions` type is used to describe how search params can be provi
type SearchParamOptions = {
search?:
| true
| Record<string, TSearchParam>
| TToSearch
| ((prev: TFromSearch) => TToSearch)
}
```
7 changes: 7 additions & 0 deletions docs/api/router/SearchSchemaInputType.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
id: SearchSchemaInputType
title: `SearchSchemaInput` type
---


The `SearchSchemaInput` type is used to tag the input type of a `validateSearch` method to signalize to TanStack router that its parameter type `TSearchSchemaInput` shall be used to type the search param of `<Link />` and `navigate()`.
5 changes: 5 additions & 0 deletions examples/react/basic-default-search-params/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
6 changes: 6 additions & 0 deletions examples/react/basic-default-search-params/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm start` or `yarn start`
13 changes: 13 additions & 0 deletions examples/react/basic-default-search-params/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions examples/react/basic-default-search-params/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "tanstack-router-react-example-basic-default-search-params",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"serve": "vite preview",
"start": "vite"
},
"dependencies": {
"@tanstack/react-query": "^5.4.3",
"@tanstack/react-router": "1.0.8",
"@tanstack/router-devtools": "1.0.8",
"@vitejs/plugin-react": "^1.1.3",
"axios": "^1.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite": "^2.8.6"
},
"devDependencies": {
"@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17"
}
}
224 changes: 224 additions & 0 deletions examples/react/basic-default-search-params/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
Outlet,
RouterProvider,
Link,
Route,
ErrorComponent,
Router,
RootRoute,
ErrorRouteProps,
NotFoundRoute,
SearchSchemaInput,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import axios from 'axios'
import { z } from 'zod'

type PostType = {
id: number
title: string
body: string
}

const fetchPosts = async () => {
console.log('Fetching posts...')
await new Promise((r) => setTimeout(r, 300))
return axios
.get<PostType[]>('https://jsonplaceholder.typicode.com/posts')
.then((r) => r.data.slice(0, 10))
}

const fetchPost = async (postId: number) => {
console.log(`Fetching post with id ${postId}...`)
await new Promise((r) => setTimeout(r, 300))
const post = await axios
.get<PostType>(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.catch((err) => {
if (err.response.status === 404) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}
throw err
})
.then((r) => r.data)

return post
}

const rootRoute = new RootRoute({
component: RootComponent,
})

function RootComponent() {
return (
<div className="bg-gradient-to-r from-green-700 to-lime-600 text-white">
<div className="p-2 flex gap-2 text-lg bg-black/40 shadow-xl">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
<Link
to={'/posts'}
activeProps={{
className: 'font-bold',
}}
>
Posts
</Link>
</div>
<Outlet />
<TanStackRouterDevtools position="bottom-right" />
</div>
)
}
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: IndexComponent,
})

function IndexComponent() {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
)
}

const postsRoute = new Route({
getParentRoute: () => rootRoute,
path: 'posts',
loader: () => fetchPosts(),
component: PostsComponent,
})

function PostsComponent() {
const posts = postsRoute.useLoaderData()

return (
<div className="p-2 flex gap-2">
<div className="list-disc bg-gray-800/70 rounded-lg divide-y divide-green-500/30">
{posts.map((post, index) => {
return (
<div key={post.id} className="whitespace-nowrap">
<Link
to={postRoute.to}
search={{
postId: post.id,
color: index % 2 ? 'red' : undefined,
}}
className="block py-1 px-2 text-green-300 hover:text-green-200"
activeProps={{ className: '!text-white font-bold' }}
>
<div>{post.title.substring(0, 20)}</div>
</Link>
</div>
)
})}
</div>
<Outlet />
</div>
)
}

const postsIndexRoute = new Route({
getParentRoute: () => postsRoute,
path: '/',
component: PostsIndexComponent,
})

function PostsIndexComponent() {
return <div>Select a post.</div>
}

class NotFoundError extends Error {}

const postRoute = new Route({
getParentRoute: () => postsRoute,
path: 'post',
validateSearch: (
input: {
postId: number
color?: 'white' | 'red' | 'green'
} & SearchSchemaInput,
) =>
z
.object({
postId: z.number().catch(1),
color: z.enum(['white', 'red', 'green']).catch('white'),
})
.parse(input),
loaderDeps: ({ search: { postId } }) => ({
postId,
}),
errorComponent: PostErrorComponent,
loader: ({ deps: { postId } }) => fetchPost(postId),
component: PostComponent,
})

function PostErrorComponent({ error }: ErrorRouteProps) {
if (error instanceof NotFoundError) {
return <div>{error.message}</div>
}

return <ErrorComponent error={error} />
}

function PostComponent() {
const post = postRoute.useLoaderData()
const { color } = postRoute.useSearch()
return (
<div className="space-y-2">
<h4 className="text-xl font-bold">{post.title}</h4>
<hr className="opacity-20" />
<div className={`text-sm text-${color}-300`}>{post.body}</div>
</div>
)
}

const notFoundRoute = new NotFoundRoute({
getParentRoute: () => rootRoute,
component: NotFound,
})

function NotFound() {
return (
<div className="p-2">
<h3>404 - Not Found</h3>
</div>
)
}

const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postRoute, postsIndexRoute]),
indexRoute,
])

// Set up a Router instance
const router = new Router({
routeTree,
notFoundRoute,
defaultPreload: 'intent',
defaultStaleTime: 5000,
})

// Register things for typesafety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)

root.render(<RouterProvider router={router} />)
}
12 changes: 12 additions & 0 deletions examples/react/basic-default-search-params/tsconfig.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"composite": true,
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./build/types"
},
"files": ["src/main.tsx"],
"include": [
"src"
// "__tests__/**/*.test.*"
]
}
7 changes: 7 additions & 0 deletions examples/react/basic-default-search-params/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react"
}
}
7 changes: 7 additions & 0 deletions examples/react/basic-default-search-params/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
20 changes: 20 additions & 0 deletions packages/react-router/src/fileRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
RootRouteId,
TrimPathLeft,
RouteConstraints,
ResolveFullSearchSchemaInput,
SearchSchemaInput,
} from './route'
import { Assign, Expand, IsAny } from './utils'

Expand Down Expand Up @@ -85,7 +87,19 @@ export class FileRoute<
constructor(public path: TFilePath) {}

createRoute = <
TSearchSchemaInput extends RouteConstraints['TSearchSchema'] = {},
TSearchSchema extends RouteConstraints['TSearchSchema'] = {},
TSearchSchemaUsed extends Record<
string,
any
> = TSearchSchemaInput extends SearchSchemaInput
? TSearchSchemaInput
: TSearchSchema,
TFullSearchSchemaInput extends
RouteConstraints['TFullSearchSchema'] = ResolveFullSearchSchemaInput<
TParentRoute,
TSearchSchemaUsed
>,
TFullSearchSchema extends
RouteConstraints['TFullSearchSchema'] = ResolveFullSearchSchema<
TParentRoute,
Expand Down Expand Up @@ -115,7 +129,10 @@ export class FileRoute<
TParentRoute,
string,
TPath,
TSearchSchemaInput,
TSearchSchema,
TSearchSchemaUsed,
TFullSearchSchemaInput,
TFullSearchSchema,
TParams,
TAllParams,
Expand All @@ -133,7 +150,10 @@ export class FileRoute<
TFullPath,
TFilePath,
TId,
TSearchSchemaInput,
TSearchSchema,
TSearchSchemaUsed,
TFullSearchSchemaInput,
TFullSearchSchema,
TParams,
TAllParams,
Expand Down
Loading