From 5414f10b356adeb051b28e713e3857cc45d7b803 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 14 Dec 2025 22:57:31 +0100 Subject: [PATCH 1/2] docs(solid-router): path params --- .../framework/solid/guide/path-params.md | 775 +++++++++++++++++- 1 file changed, 762 insertions(+), 13 deletions(-) diff --git a/docs/router/framework/solid/guide/path-params.md b/docs/router/framework/solid/guide/path-params.md index 2e4fb060b9a..bac7b869fc7 100644 --- a/docs/router/framework/solid/guide/path-params.md +++ b/docs/router/framework/solid/guide/path-params.md @@ -1,15 +1,764 @@ --- -ref: docs/router/framework/react/guide/path-params.md -replace: - { - 'react-router': 'solid-router', - '{ postId } = Route.useParams()': 'params = Route.useParams', - '{ fileName } = Route.useParams()': 'params = Route.useParams', - '{ userId } = Route.useParams()': 'params = Route.useParams', - '{ _splat } = Route.useParams()': 'params = Route.useParams', - '{postId}': '{params().postId}', - '{userId}': '{params().userId}', - '{fileName}': '{params().userId}', - '{_splat}': '{params()._splat}', - } +title: Path Params --- + +Path params are used to match a single segment (the text until the next `/`) and provide its value back to you as a **named** variable. They are defined by using the `$` character prefix in the path, followed by the key variable to assign it to. The following are valid path param paths: + +- `$postId` +- `$name` +- `$teamId` +- `about/$name` +- `team/$teamId` +- `blog/$postId` + +Because path param routes only match to the next `/`, child routes can be created to continue expressing hierarchy: + +Let's create a post route file that uses a path param to match the post ID: + +- `posts.$postId.tsx` + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + return fetchPost(params.postId) + }, +}) +``` + +## Path Params can be used by child routes + +Once a path param has been parsed, it is available to all child routes. This means that if we define a child route to our `postRoute`, we can use the `postId` variable from the URL in the child route's path! + +## Path Params in Loaders + +Path params are passed to the loader as a `params` object. The keys of this object are the names of the path params, and the values are the values that were parsed out of the actual URL path. For example, if we were to visit the `/blog/123` URL, the `params` object would be `{ postId: '123' }`: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + return fetchPost(params.postId) + }, +}) +``` + +The `params` object is also passed to the `beforeLoad` option: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + beforeLoad: async ({ params }) => { + // do something with params.postId + }, +}) +``` + +## Path Params in Components + +If we add a component to our `postRoute`, we can access the `postId` variable from the URL by using the route's `useParams` hook: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + component: PostComponent, +}) + +function PostComponent() { + const params = Route.useParams() + return
Post {params().postId}
+} +``` + +> 🧠 Quick tip: If your component is code-split, you can use the [getRouteApi function](./code-splitting.md#manually-accessing-route-apis-in-other-files-with-the-getrouteapi-helper) to avoid having to import the `Route` configuration to get access to the typed `useParams()` hook. + +## Path Params outside of Routes + +You can also use the globally exported `useParams` hook to access any parsed path params from any component in your app. You'll need to pass the `strict: false` option to `useParams`, denoting that you want to access the params from an ambiguous location: + +```tsx +function PostComponent() { + const params = useParams({ strict: false }) + return
Post {params().postId}
+} +``` + +## Navigating with Path Params + +When navigating to a route with path params, TypeScript will require you to pass the params either as an object or as a function that returns an object of params. + +Let's see what an object style looks like: + +```tsx +function Component() { + return ( + + Post 123 + + ) +} +``` + +And here's what a function style looks like: + +```tsx +function Component() { + return ( + ({ ...prev, postId: '123' })}> + Post 123 + + ) +} +``` + +Notice that the function style is useful when you need to persist params that are already in the URL for other routes. This is because the function style will receive the current params as an argument, allowing you to modify them as needed and return the final params object. + +## Prefixes and Suffixes for Path Params + +You can also use **prefixes** and **suffixes** with path params to create more complex routing patterns. This allows you to match specific URL structures while still capturing the dynamic segments. + +When using either prefixes or suffixes, you can define them by wrapping the path param in curly braces `{}` and placing the prefix or suffix before or after the variable name. + +### Defining Prefixes + +Prefixes are defined by placing the prefix text outside the curly braces before the variable name. For example, if you want to match a URL that starts with `post-` followed by a post ID, you can define it like this: + +```tsx +// src/routes/posts/post-{$postId}.tsx +export const Route = createFileRoute('/posts/post-{$postId}')({ + component: PostComponent, +}) + +function PostComponent() { + const params = Route.useParams() + // postId will be the value after 'post-' + return
Post ID: {params().postId}
+} +``` + +You can even combines prefixes with wildcard routes to create more complex patterns: + +```tsx +// src/routes/on-disk/storage-{$} +export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({ + component: StorageComponent, +}) + +function StorageComponent() { + const params = Route.useParams() + // _splat, will be value after 'storage-' + // i.e. my-drive/documents/foo.txt + return
Storage Location: /{params()._splat}
+} +``` + +### Defining Suffixes + +Suffixes are defined by placing the suffix text outside the curly braces after the variable name. For example, if you want to match a URL a filename that ends with `txt`, you can define it like this: + +```tsx +// src/routes/files/{$fileName}txt +export const Route = createFileRoute('/files/{$fileName}.txt')({ + component: FileComponent, +}) + +function FileComponent() { + const params = Route.useParams() + // fileName will be the value before 'txt' + return
File Name: {params().fileName}
+} +``` + +You can also combine suffixes with wildcards for more complex routing patterns: + +```tsx +// src/routes/files/{$}[.]txt +export const Route = createFileRoute('/files/{$fileName}[.]txt')({ + component: FileComponent, +}) + +function FileComponent() { + const params = Route.useParams() + // _splat will be the value before '.txt' + return
File Splat: {params()._splat}
+} +``` + +### Combining Prefixes and Suffixes + +You can combine both prefixes and suffixes to create very specific routing patterns. For example, if you want to match a URL that starts with `user-` and ends with `.json`, you can define it like this: + +```tsx +// src/routes/users/user-{$userId}.json +export const Route = createFileRoute('/users/user-{$userId}.json')({ + component: UserComponent, +}) + +function UserComponent() { + const params = Route.useParams() + // userId will be the value between 'user-' and '.json' + return
User ID: {params().userId}
+} +``` + +Similar to the previous examples, you can also use wildcards with prefixes and suffixes. Go wild! + +## Optional Path Parameters + +Optional path parameters allow you to define route segments that may or may not be present in the URL. They use the `{-$paramName}` syntax and provide flexible routing patterns where certain parameters are optional. + +### Defining Optional Parameters + +Optional path parameters are defined using curly braces with a dash prefix: `{-$paramName}` + +```tsx +// Single optional parameter +// src/routes/posts/{-$category}.tsx +export const Route = createFileRoute('/posts/{-$category}')({ + component: PostsComponent, +}) + +// Multiple optional parameters +// src/routes/posts/{-$category}/{-$slug}.tsx +export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({ + component: PostComponent, +}) + +// Mixed required and optional parameters +// src/routes/users/$id/{-$tab}.tsx +export const Route = createFileRoute('/users/$id/{-$tab}')({ + component: UserComponent, +}) +``` + +### How Optional Parameters Work + +Optional parameters create flexible URL patterns: + +- `/posts/{-$category}` matches both `/posts` and `/posts/tech` +- `/posts/{-$category}/{-$slug}` matches `/posts`, `/posts/tech`, and `/posts/tech/hello-world` +- `/users/$id/{-$tab}` matches `/users/123` and `/users/123/settings` + +When an optional parameter is not present in the URL, its value will be `undefined` in your route handlers and components. + +### Accessing Optional Parameters + +Optional parameters work exactly like regular parameters in your components, but their values may be `undefined`: + +```tsx +function PostsComponent() { + const params = Route.useParams() + + return
{params().category ? `Posts in ${params().category}` : 'All Posts'}
+} +``` + +### Optional Parameters in Loaders + +Optional parameters are available in loaders and may be `undefined`: + +```tsx +export const Route = createFileRoute('/posts/{-$category}')({ + loader: async ({ params }) => { + // params.category might be undefined + return fetchPosts({ category: params.category }) + }, +}) +``` + +### Optional Parameters in beforeLoad + +Optional parameters work in `beforeLoad` handlers as well: + +```tsx +export const Route = createFileRoute('/posts/{-$category}')({ + beforeLoad: async ({ params }) => { + if (params.category) { + // Validate category exists + await validateCategory(params.category) + } + }, +}) +``` + +### Advanced Optional Parameter Patterns + +#### With Prefix and Suffix + +Optional parameters support prefix and suffix patterns: + +```tsx +// File route: /files/prefix{-$name}.txt +// Matches: /files/prefix.txt and /files/prefixdocument.txt +export const Route = createFileRoute('/files/prefix{-$name}.txt')({ + component: FileComponent, +}) + +function FileComponent() { + const params = Route.useParams() + return
File: {params().name || 'default'}
+} +``` + +#### All Optional Parameters + +You can create routes where all parameters are optional: + +```tsx +// Route: /{-$year}/{-$month}/{-$day} +// Matches: /, /2023, /2023/12, /2023/12/25 +export const Route = createFileRoute('/{-$year}/{-$month}/{-$day}')({ + component: DateComponent, +}) + +function DateComponent() { + const params = Route.useParams() + + if (!params().year) return
Select a year
+ if (!params().month) return
Year: {params().year}
+ if (!params().day) + return ( +
+ Month: {params().year}/{params().month} +
+ ) + + return ( +
+ Date: {params().year}/{params().month}/{params().day} +
+ ) +} +``` + +#### Optional Parameters with Wildcards + +Optional parameters can be combined with wildcards for complex routing patterns: + +```tsx +// Route: /docs/{-$version}/$ +// Matches: /docs/extra/path, /docs/v2/extra/path +export const Route = createFileRoute('/docs/{-$version}/$')({ + component: DocsComponent, +}) + +function DocsComponent() { + const params = Route.useParams() + + return ( +
+ Version: {params().version || 'latest'} + Path: {params()._splat} +
+ ) +} +``` + +### Navigating with Optional Parameters + +When navigating to routes with optional parameters, you have fine-grained control over which parameters to include: + +```tsx +function Navigation() { + return ( +
+ {/* Navigate with optional parameter */} + + Tech Posts + + + {/* Navigate without optional parameter */} + + All Posts + + + {/* Navigate with multiple optional parameters */} + + Specific Post + +
+ ) +} +``` + +### Type Safety with Optional Parameters + +TypeScript provides full type safety for optional parameters: + +```tsx +function PostsComponent() { + // TypeScript knows category might be undefined + const params = Route.useParams() // category: string | undefined + + // Safe navigation + const categoryUpper = params().category?.toUpperCase() + + return
{categoryUpper || 'All Categories'}
+} + +// Navigation is type-safe and flexible + + Tech Posts + + + + Category 123 + +``` + +## Internationalization (i18n) with Optional Path Parameters + +Optional path parameters are excellent for implementing internationalization (i18n) routing patterns. You can use prefix patterns to handle multiple languages while maintaining clean, SEO-friendly URLs. + +### Prefix-based i18n + +Use optional language prefixes to support URLs like `/en/about`, `/fr/about`, or just `/about` (default language): + +```tsx +// Route: /{-$locale}/about +export const Route = createFileRoute('/{-$locale}/about')({ + component: AboutComponent, +}) + +function AboutComponent() { + const params = Route.useParams() + const currentLocale = params().locale || 'en' // Default to English + + const content = { + en: { title: 'About Us', description: 'Learn more about our company.' }, + fr: { + title: 'À Propos', + description: 'En savoir plus sur notre entreprise.', + }, + es: { + title: 'Acerca de', + description: 'Conoce más sobre nuestra empresa.', + }, + } + + return ( +
+

{content[currentLocale]?.title}

+

{content[currentLocale]?.description}

+
+ ) +} +``` + +This pattern matches: + +- `/about` (default locale) +- `/en/about` (explicit English) +- `/fr/about` (French) +- `/es/about` (Spanish) + +### Complex i18n Patterns + +Combine optional parameters for more sophisticated i18n routing: + +```tsx +// Route: /{-$locale}/blog/{-$category}/$slug +export const Route = createFileRoute('/{-$locale}/blog/{-$category}/$slug')({ + beforeLoad: async ({ params }) => { + const locale = params.locale || 'en' + const category = params.category + + // Validate locale and category + const validLocales = ['en', 'fr', 'es', 'de'] + if (locale && !validLocales.includes(locale)) { + throw new Error('Invalid locale') + } + + return { locale, category } + }, + loader: async ({ params, context }) => { + const { locale } = context + const { slug, category } = params + + return fetchBlogPost({ slug, category, locale }) + }, + component: BlogPostComponent, +}) + +function BlogPostComponent() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{data.title}

+

+ Category: {params().category || 'All'} | Language: {params().locale || 'en'} +

+
{data.content}
+
+ ) +} +``` + +This supports URLs like: + +- `/blog/tech/my-post` (default locale, tech category) +- `/fr/blog/my-post` (French, no category) +- `/en/blog/tech/my-post` (explicit English, tech category) +- `/es/blog/tecnologia/mi-post` (Spanish, Spanish category) + +### Language Navigation + +Create language switchers using optional i18n parameters with function-style params: + +```tsx +function LanguageSwitcher() { + const currentParams = useParams({ strict: false }) + + const languages = [ + { code: 'en', name: 'English' }, + { code: 'fr', name: 'Français' }, + { code: 'es', name: 'Español' }, + ] + + return ( +
+ {languages.map(({ code, name }) => ( + ({ + ...prev, + locale: code === 'en' ? undefined : code, // Remove 'en' for clean URLs + })} + className={currentParams().locale === code ? 'active' : ''} + > + {name} + + ))} +
+ ) +} +``` + +You can also create more sophisticated language switching logic: + +```tsx +function AdvancedLanguageSwitcher() { + const currentParams = useParams({ strict: false }) + + const handleLanguageChange = (newLocale: string) => { + return (prev: any) => { + // Preserve all existing params but update locale + const updatedParams = { ...prev } + + if (newLocale === 'en') { + // Remove locale for clean English URLs + delete updatedParams.locale + } else { + updatedParams.locale = newLocale + } + + return updatedParams + } + } + + return ( +
+ + Français + + + + Español + + + + English + +
+ ) +} +``` + +### Advanced i18n with Optional Parameters + +Organize i18n routes using optional parameters for flexible locale handling: + +```tsx +// Route structure: +// routes/ +// {-$locale}/ +// index.tsx // /, /en, /fr +// about.tsx // /about, /en/about, /fr/about +// blog/ +// index.tsx // /blog, /en/blog, /fr/blog +// $slug.tsx // /blog/post, /en/blog/post, /fr/blog/post + +// routes/{-$locale}/index.tsx +export const Route = createFileRoute('/{-$locale}/')({ + component: HomeComponent, +}) + +function HomeComponent() { + const params = Route.useParams() + const isRTL = ['ar', 'he', 'fa'].includes(locale || '') + + return ( +
+

Welcome ({params().locale || 'en'})

+ {/* Localized content */} +
+ ) +} +``` + +```tsx +// routes/{-$locale}/about.tsx +export const Route = createFileRoute('/{-$locale}/about')({ + component: AboutComponent, +}) +``` + +### SEO and Canonical URLs + +Handle SEO for i18n routes properly: + +```tsx +export const Route = createFileRoute('/{-$locale}/products/$id')({ + component: ProductComponent, + head: ({ params, loaderData }) => { + const locale = params.locale || 'en' + const product = loaderData + + return { + title: product.title[locale] || product.title.en, + meta: [ + { + name: 'description', + content: product.description[locale] || product.description.en, + }, + { + property: 'og:locale', + content: locale, + }, + ], + links: [ + // Canonical URL (always use default locale format) + { + rel: 'canonical', + href: `https://example.com/products/${params.id}`, + }, + // Alternate language versions + { + rel: 'alternate', + hreflang: 'en', + href: `https://example.com/products/${params.id}`, + }, + { + rel: 'alternate', + hreflang: 'fr', + href: `https://example.com/fr/products/${params.id}`, + }, + { + rel: 'alternate', + hreflang: 'es', + href: `https://example.com/es/products/${params.id}`, + }, + ], + } + }, +}) +``` + +### Type Safety for i18n + +Ensure type safety for your i18n implementations: + +```tsx +// Define supported locales +type Locale = 'en' | 'fr' | 'es' | 'de' + +// Type-safe locale validation +function validateLocale(locale: string | undefined): locale is Locale { + return ['en', 'fr', 'es', 'de'].includes(locale as Locale) +} + +export const Route = createFileRoute('/{-$locale}/shop/{-$category}')({ + beforeLoad: async ({ params }) => { + const { locale } = params + + // Type-safe locale validation + if (locale && !validateLocale(locale)) { + throw redirect({ + to: '/shop/{-$category}', + params: { category: params.category }, + }) + } + + return { + locale: (locale as Locale) || 'en', + isDefaultLocale: !locale || locale === 'en', + } + }, + component: ShopComponent, +}) + +function ShopComponent() { + const params = Route.useParams() + const routeContext = Route.useRouteContext() + + // TypeScript knows locale is Locale | undefined + // and we have validated it in beforeLoad + + return ( +
+

Shop {params().category ? `- ${params().category}` : ''}

+

Language: {params().locale || 'en'}

+ {!routeContext().isDefaultLocale && ( + + View in English + + )} +
+ ) +} +``` + +Optional path parameters provide a powerful and flexible foundation for implementing internationalization in your TanStack Router applications. Whether you prefer prefix-based or combined approaches, you can create clean, SEO-friendly URLs while maintaining excellent developer experience and type safety. + +## Allowed Characters + +By default, path params are escaped with `encodeURIComponent`. If you want to allow other valid URI characters (e.g. `@` or `+`), you can specify that in your [RouterOptions](../api/router/RouterOptionsType.md#pathparamsallowedcharacters-property). + +Example usage: + +```tsx +const router = createRouter({ + // ... + pathParamsAllowedCharacters: ['@'], +}) +``` + +The following is the list of accepted allowed characters: + +- `;` +- `:` +- `@` +- `&` +- `=` +- `+` +- `$` +- `,` From dfd784d08cb5854c4590ef3f6a77db0cbca33099 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:02:42 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../framework/solid/guide/path-params.md | 575 +++++++++--------- 1 file changed, 290 insertions(+), 285 deletions(-) diff --git a/docs/router/framework/solid/guide/path-params.md b/docs/router/framework/solid/guide/path-params.md index bac7b869fc7..769f4746216 100644 --- a/docs/router/framework/solid/guide/path-params.md +++ b/docs/router/framework/solid/guide/path-params.md @@ -21,9 +21,9 @@ Let's create a post route file that uses a path param to match the post ID: import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/posts/$postId')({ - loader: async ({ params }) => { - return fetchPost(params.postId) - }, + loader: async ({ params }) => { + return fetchPost(params.postId) + }, }) ``` @@ -37,9 +37,9 @@ Path params are passed to the loader as a `params` object. The keys of this obje ```tsx export const Route = createFileRoute('/posts/$postId')({ - loader: async ({ params }) => { - return fetchPost(params.postId) - }, + loader: async ({ params }) => { + return fetchPost(params.postId) + }, }) ``` @@ -47,9 +47,9 @@ The `params` object is also passed to the `beforeLoad` option: ```tsx export const Route = createFileRoute('/posts/$postId')({ - beforeLoad: async ({ params }) => { - // do something with params.postId - }, + beforeLoad: async ({ params }) => { + // do something with params.postId + }, }) ``` @@ -59,12 +59,12 @@ If we add a component to our `postRoute`, we can access the `postId` variable fr ```tsx export const Route = createFileRoute('/posts/$postId')({ - component: PostComponent, + component: PostComponent, }) function PostComponent() { - const params = Route.useParams() - return
Post {params().postId}
+ const params = Route.useParams() + return
Post {params().postId}
} ``` @@ -76,8 +76,8 @@ You can also use the globally exported `useParams` hook to access any parsed pat ```tsx function PostComponent() { - const params = useParams({ strict: false }) - return
Post {params().postId}
+ const params = useParams({ strict: false }) + return
Post {params().postId}
} ``` @@ -89,11 +89,11 @@ Let's see what an object style looks like: ```tsx function Component() { - return ( - - Post 123 - - ) + return ( + + Post 123 + + ) } ``` @@ -101,11 +101,11 @@ And here's what a function style looks like: ```tsx function Component() { - return ( - ({ ...prev, postId: '123' })}> - Post 123 - - ) + return ( + ({ ...prev, postId: '123' })}> + Post 123 + + ) } ``` @@ -124,13 +124,13 @@ Prefixes are defined by placing the prefix text outside the curly braces before ```tsx // src/routes/posts/post-{$postId}.tsx export const Route = createFileRoute('/posts/post-{$postId}')({ - component: PostComponent, + component: PostComponent, }) function PostComponent() { - const params = Route.useParams() - // postId will be the value after 'post-' - return
Post ID: {params().postId}
+ const params = Route.useParams() + // postId will be the value after 'post-' + return
Post ID: {params().postId}
} ``` @@ -139,14 +139,14 @@ You can even combines prefixes with wildcard routes to create more complex patte ```tsx // src/routes/on-disk/storage-{$} export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({ - component: StorageComponent, + component: StorageComponent, }) function StorageComponent() { - const params = Route.useParams() - // _splat, will be value after 'storage-' - // i.e. my-drive/documents/foo.txt - return
Storage Location: /{params()._splat}
+ const params = Route.useParams() + // _splat, will be value after 'storage-' + // i.e. my-drive/documents/foo.txt + return
Storage Location: /{params()._splat}
} ``` @@ -157,13 +157,13 @@ Suffixes are defined by placing the suffix text outside the curly braces after t ```tsx // src/routes/files/{$fileName}txt export const Route = createFileRoute('/files/{$fileName}.txt')({ - component: FileComponent, + component: FileComponent, }) function FileComponent() { - const params = Route.useParams() - // fileName will be the value before 'txt' - return
File Name: {params().fileName}
+ const params = Route.useParams() + // fileName will be the value before 'txt' + return
File Name: {params().fileName}
} ``` @@ -172,13 +172,13 @@ You can also combine suffixes with wildcards for more complex routing patterns: ```tsx // src/routes/files/{$}[.]txt export const Route = createFileRoute('/files/{$fileName}[.]txt')({ - component: FileComponent, + component: FileComponent, }) function FileComponent() { - const params = Route.useParams() - // _splat will be the value before '.txt' - return
File Splat: {params()._splat}
+ const params = Route.useParams() + // _splat will be the value before '.txt' + return
File Splat: {params()._splat}
} ``` @@ -189,13 +189,13 @@ You can combine both prefixes and suffixes to create very specific routing patte ```tsx // src/routes/users/user-{$userId}.json export const Route = createFileRoute('/users/user-{$userId}.json')({ - component: UserComponent, + component: UserComponent, }) function UserComponent() { - const params = Route.useParams() - // userId will be the value between 'user-' and '.json' - return
User ID: {params().userId}
+ const params = Route.useParams() + // userId will be the value between 'user-' and '.json' + return
User ID: {params().userId}
} ``` @@ -213,19 +213,19 @@ Optional path parameters are defined using curly braces with a dash prefix: `{-$ // Single optional parameter // src/routes/posts/{-$category}.tsx export const Route = createFileRoute('/posts/{-$category}')({ - component: PostsComponent, + component: PostsComponent, }) // Multiple optional parameters // src/routes/posts/{-$category}/{-$slug}.tsx export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({ - component: PostComponent, + component: PostComponent, }) // Mixed required and optional parameters // src/routes/users/$id/{-$tab}.tsx export const Route = createFileRoute('/users/$id/{-$tab}')({ - component: UserComponent, + component: UserComponent, }) ``` @@ -245,9 +245,13 @@ Optional parameters work exactly like regular parameters in your components, but ```tsx function PostsComponent() { - const params = Route.useParams() + const params = Route.useParams() - return
{params().category ? `Posts in ${params().category}` : 'All Posts'}
+ return ( +
+ {params().category ? `Posts in ${params().category}` : 'All Posts'} +
+ ) } ``` @@ -257,10 +261,10 @@ Optional parameters are available in loaders and may be `undefined`: ```tsx export const Route = createFileRoute('/posts/{-$category}')({ - loader: async ({ params }) => { - // params.category might be undefined - return fetchPosts({ category: params.category }) - }, + loader: async ({ params }) => { + // params.category might be undefined + return fetchPosts({ category: params.category }) + }, }) ``` @@ -270,12 +274,12 @@ Optional parameters work in `beforeLoad` handlers as well: ```tsx export const Route = createFileRoute('/posts/{-$category}')({ - beforeLoad: async ({ params }) => { - if (params.category) { - // Validate category exists - await validateCategory(params.category) - } - }, + beforeLoad: async ({ params }) => { + if (params.category) { + // Validate category exists + await validateCategory(params.category) + } + }, }) ``` @@ -289,12 +293,12 @@ Optional parameters support prefix and suffix patterns: // File route: /files/prefix{-$name}.txt // Matches: /files/prefix.txt and /files/prefixdocument.txt export const Route = createFileRoute('/files/prefix{-$name}.txt')({ - component: FileComponent, + component: FileComponent, }) function FileComponent() { - const params = Route.useParams() - return
File: {params().name || 'default'}
+ const params = Route.useParams() + return
File: {params().name || 'default'}
} ``` @@ -306,26 +310,26 @@ You can create routes where all parameters are optional: // Route: /{-$year}/{-$month}/{-$day} // Matches: /, /2023, /2023/12, /2023/12/25 export const Route = createFileRoute('/{-$year}/{-$month}/{-$day}')({ - component: DateComponent, + component: DateComponent, }) function DateComponent() { - const params = Route.useParams() - - if (!params().year) return
Select a year
- if (!params().month) return
Year: {params().year}
- if (!params().day) - return ( -
- Month: {params().year}/{params().month} -
- ) - - return ( -
- Date: {params().year}/{params().month}/{params().day} -
- ) + const params = Route.useParams() + + if (!params().year) return
Select a year
+ if (!params().month) return
Year: {params().year}
+ if (!params().day) + return ( +
+ Month: {params().year}/{params().month} +
+ ) + + return ( +
+ Date: {params().year}/{params().month}/{params().day} +
+ ) } ``` @@ -337,18 +341,18 @@ Optional parameters can be combined with wildcards for complex routing patterns: // Route: /docs/{-$version}/$ // Matches: /docs/extra/path, /docs/v2/extra/path export const Route = createFileRoute('/docs/{-$version}/$')({ - component: DocsComponent, + component: DocsComponent, }) function DocsComponent() { - const params = Route.useParams() - - return ( -
- Version: {params().version || 'latest'} - Path: {params()._splat} -
- ) + const params = Route.useParams() + + return ( +
+ Version: {params().version || 'latest'} + Path: {params()._splat} +
+ ) } ``` @@ -358,27 +362,27 @@ When navigating to routes with optional parameters, you have fine-grained contro ```tsx function Navigation() { - return ( -
- {/* Navigate with optional parameter */} - - Tech Posts - - - {/* Navigate without optional parameter */} - - All Posts - - - {/* Navigate with multiple optional parameters */} - - Specific Post - -
- ) + return ( +
+ {/* Navigate with optional parameter */} + + Tech Posts + + + {/* Navigate without optional parameter */} + + All Posts + + + {/* Navigate with multiple optional parameters */} + + Specific Post + +
+ ) } ``` @@ -424,31 +428,31 @@ Use optional language prefixes to support URLs like `/en/about`, `/fr/about`, or ```tsx // Route: /{-$locale}/about export const Route = createFileRoute('/{-$locale}/about')({ - component: AboutComponent, + component: AboutComponent, }) function AboutComponent() { - const params = Route.useParams() - const currentLocale = params().locale || 'en' // Default to English - - const content = { - en: { title: 'About Us', description: 'Learn more about our company.' }, - fr: { - title: 'À Propos', - description: 'En savoir plus sur notre entreprise.', - }, - es: { - title: 'Acerca de', - description: 'Conoce más sobre nuestra empresa.', - }, - } - - return ( -
-

{content[currentLocale]?.title}

-

{content[currentLocale]?.description}

-
- ) + const params = Route.useParams() + const currentLocale = params().locale || 'en' // Default to English + + const content = { + en: { title: 'About Us', description: 'Learn more about our company.' }, + fr: { + title: 'À Propos', + description: 'En savoir plus sur notre entreprise.', + }, + es: { + title: 'Acerca de', + description: 'Conoce más sobre nuestra empresa.', + }, + } + + return ( +
+

{content[currentLocale]?.title}

+

{content[currentLocale]?.description}

+
+ ) } ``` @@ -466,40 +470,41 @@ Combine optional parameters for more sophisticated i18n routing: ```tsx // Route: /{-$locale}/blog/{-$category}/$slug export const Route = createFileRoute('/{-$locale}/blog/{-$category}/$slug')({ - beforeLoad: async ({ params }) => { - const locale = params.locale || 'en' - const category = params.category - - // Validate locale and category - const validLocales = ['en', 'fr', 'es', 'de'] - if (locale && !validLocales.includes(locale)) { - throw new Error('Invalid locale') - } - - return { locale, category } - }, - loader: async ({ params, context }) => { - const { locale } = context - const { slug, category } = params - - return fetchBlogPost({ slug, category, locale }) - }, - component: BlogPostComponent, + beforeLoad: async ({ params }) => { + const locale = params.locale || 'en' + const category = params.category + + // Validate locale and category + const validLocales = ['en', 'fr', 'es', 'de'] + if (locale && !validLocales.includes(locale)) { + throw new Error('Invalid locale') + } + + return { locale, category } + }, + loader: async ({ params, context }) => { + const { locale } = context + const { slug, category } = params + + return fetchBlogPost({ slug, category, locale }) + }, + component: BlogPostComponent, }) function BlogPostComponent() { - const params = Route.useParams() - const data = Route.useLoaderData() - - return ( -
-

{data.title}

-

- Category: {params().category || 'All'} | Language: {params().locale || 'en'} -

-
{data.content}
-
- ) + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{data.title}

+

+ Category: {params().category || 'All'} | Language:{' '} + {params().locale || 'en'} +

+
{data.content}
+
+ ) } ``` @@ -516,31 +521,31 @@ Create language switchers using optional i18n parameters with function-style par ```tsx function LanguageSwitcher() { - const currentParams = useParams({ strict: false }) - - const languages = [ - { code: 'en', name: 'English' }, - { code: 'fr', name: 'Français' }, - { code: 'es', name: 'Español' }, - ] - - return ( -
- {languages.map(({ code, name }) => ( - ({ - ...prev, - locale: code === 'en' ? undefined : code, // Remove 'en' for clean URLs - })} - className={currentParams().locale === code ? 'active' : ''} - > - {name} - - ))} -
- ) + const currentParams = useParams({ strict: false }) + + const languages = [ + { code: 'en', name: 'English' }, + { code: 'fr', name: 'Français' }, + { code: 'es', name: 'Español' }, + ] + + return ( +
+ {languages.map(({ code, name }) => ( + ({ + ...prev, + locale: code === 'en' ? undefined : code, // Remove 'en' for clean URLs + })} + className={currentParams().locale === code ? 'active' : ''} + > + {name} + + ))} +
+ ) } ``` @@ -548,48 +553,48 @@ You can also create more sophisticated language switching logic: ```tsx function AdvancedLanguageSwitcher() { - const currentParams = useParams({ strict: false }) - - const handleLanguageChange = (newLocale: string) => { - return (prev: any) => { - // Preserve all existing params but update locale - const updatedParams = { ...prev } - - if (newLocale === 'en') { - // Remove locale for clean English URLs - delete updatedParams.locale - } else { - updatedParams.locale = newLocale - } - - return updatedParams - } - } - - return ( -
- - Français - - - - Español - - - - English - -
- ) + const currentParams = useParams({ strict: false }) + + const handleLanguageChange = (newLocale: string) => { + return (prev: any) => { + // Preserve all existing params but update locale + const updatedParams = { ...prev } + + if (newLocale === 'en') { + // Remove locale for clean English URLs + delete updatedParams.locale + } else { + updatedParams.locale = newLocale + } + + return updatedParams + } + } + + return ( +
+ + Français + + + + Español + + + + English + +
+ ) } ``` @@ -609,26 +614,26 @@ Organize i18n routes using optional parameters for flexible locale handling: // routes/{-$locale}/index.tsx export const Route = createFileRoute('/{-$locale}/')({ - component: HomeComponent, + component: HomeComponent, }) function HomeComponent() { - const params = Route.useParams() - const isRTL = ['ar', 'he', 'fa'].includes(locale || '') - - return ( -
-

Welcome ({params().locale || 'en'})

- {/* Localized content */} -
- ) + const params = Route.useParams() + const isRTL = ['ar', 'he', 'fa'].includes(locale || '') + + return ( +
+

Welcome ({params().locale || 'en'})

+ {/* Localized content */} +
+ ) } ``` ```tsx // routes/{-$locale}/about.tsx export const Route = createFileRoute('/{-$locale}/about')({ - component: AboutComponent, + component: AboutComponent, }) ``` @@ -638,48 +643,48 @@ Handle SEO for i18n routes properly: ```tsx export const Route = createFileRoute('/{-$locale}/products/$id')({ - component: ProductComponent, - head: ({ params, loaderData }) => { - const locale = params.locale || 'en' - const product = loaderData - - return { - title: product.title[locale] || product.title.en, - meta: [ - { - name: 'description', - content: product.description[locale] || product.description.en, - }, - { - property: 'og:locale', - content: locale, - }, - ], - links: [ - // Canonical URL (always use default locale format) - { - rel: 'canonical', - href: `https://example.com/products/${params.id}`, - }, - // Alternate language versions - { - rel: 'alternate', - hreflang: 'en', - href: `https://example.com/products/${params.id}`, - }, - { - rel: 'alternate', - hreflang: 'fr', - href: `https://example.com/fr/products/${params.id}`, - }, - { - rel: 'alternate', - hreflang: 'es', - href: `https://example.com/es/products/${params.id}`, - }, - ], - } - }, + component: ProductComponent, + head: ({ params, loaderData }) => { + const locale = params.locale || 'en' + const product = loaderData + + return { + title: product.title[locale] || product.title.en, + meta: [ + { + name: 'description', + content: product.description[locale] || product.description.en, + }, + { + property: 'og:locale', + content: locale, + }, + ], + links: [ + // Canonical URL (always use default locale format) + { + rel: 'canonical', + href: `https://example.com/products/${params.id}`, + }, + // Alternate language versions + { + rel: 'alternate', + hreflang: 'en', + href: `https://example.com/products/${params.id}`, + }, + { + rel: 'alternate', + hreflang: 'fr', + href: `https://example.com/fr/products/${params.id}`, + }, + { + rel: 'alternate', + hreflang: 'es', + href: `https://example.com/es/products/${params.id}`, + }, + ], + } + }, }) ``` @@ -747,8 +752,8 @@ Example usage: ```tsx const router = createRouter({ - // ... - pathParamsAllowedCharacters: ['@'], + // ... + pathParamsAllowedCharacters: ['@'], }) ```