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
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ File-based routing requires that you follow a few simple file naming conventions

> **💡 Remember:** The file-naming conventions for your project could be affected by what [options](../../../api/file-based-routing.md) are configured.

> [!NOTE]
> To escape a trailing underscore, for example `/posts[_].tsx`, usage of the upgraded [Non-Nested Routes](./routing-concepts#non-nested-routes) is required.

## Dynamic Path Params

Dynamic path params can be used in both flat and directory routes to create routes that can match a dynamic segment of the URL path. Dynamic path params are denoted by the `$` character in the filename:
Expand Down
34 changes: 0 additions & 34 deletions docs/router/framework/react/routing/routing-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,40 +352,6 @@ The following table shows which component will be rendered based on the URL:
- The `posts.$postId.tsx` route is nested as normal under the `posts.tsx` route and will render `<Posts><Post>`.
- The `posts_.$postId.edit.tsx` route **does not share** the same `posts` prefix as the other routes and therefore will be treated as if it is a top-level route and will render `<PostEditor>`.

> [!NOTE]
> While using non-nested routes with file-based routing already works brilliantly, it might misbehave in certain conditions.
> Many of these limitations have already been addressed and will be released in the next major version of TanStack Router.
>
> To start enjoying these benefits early, you can enable the experimental `nonNestedRoutes` flag in the router plugin configuration:
>
> ```ts
> export default defineConfig({
> plugins: [
> tanstackRouter({
> // some config,
> experimental: {
> nonNestedRoutes: true,
> },
> }),
> ],
> })
> ```
>
> _It is important to note that this does bring a slight change in how non-nested routes are referenced in useParams, useNavigate, etc. For this reason this has been released as a feature flag.
> The trailing underscore is no longer expected in the path:_
>
> Previously:
>
> ```ts
> useParams({ from: '/posts_/$postId/edit' })
> ```
>
> Now:
>
> ```ts
> useParams({ from: '/posts/$postId/edit' })
> ```

## Excluding Files and Folders from Routes

Files and folders can be excluded from route generation with a `-` prefix attached to the file name. This gives you the ability to colocate logic in the route directories.
Expand Down
9 changes: 2 additions & 7 deletions e2e/react-router/basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:nonnested": "MODE=nonnested VITE_MODE=nonnested vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"build:nonnested": "MODE=nonnested VITE_MODE=nonnested vite build && tsc --noEmit",
"preview": "vite preview",
"preview:nonnested": "MODE=nonnested VITE_MODE=nonnested vite preview",
"start": "vite",
"start:nonnested": "MODE=nonnested VITE_MODE=nonnested vite",
"test:e2e": "pnpm run test:e2e:nonnested && pnpm run test:e2e:default",
"test:e2e:default": "rm -rf port*.txt; playwright test --project=chromium",
"test:e2e:nonnested": "rm -rf port*.txt; MODE=nonnested playwright test --project=chromium"
"test:e2e": "pnpm run test:e2e:default",
"test:e2e:default": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.15",
Expand Down
7 changes: 1 addition & 6 deletions e2e/react-router/basic-file-based/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import {
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }
import { useExperimentalNonNestedRoutes } from './tests/utils/useExperimentalNonNestedRoutes'

const PORT = await getTestServerPort(packageJson.name)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
const experimentalNonNestedPathsModeCommand = `pnpm build:nonnested && pnpm preview:nonnested --port ${PORT}`
const defaultCommand = `pnpm build && pnpm preview --port ${PORT}`
const command = useExperimentalNonNestedRoutes
? experimentalNonNestedPathsModeCommand
: defaultCommand
const command = `pnpm build && pnpm preview --port ${PORT}`

console.info('Running with mode: ', process.env.MODE || 'default')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { createFileRoute, useParams } from '@tanstack/react-router'
import { useExperimentalNonNestedRoutes } from '../../../../../tests/utils/useExperimentalNonNestedRoutes'

export const Route = createFileRoute('/params-ps/non-nested/$foo_/$bar')({
component: RouteComponent,
})

function RouteComponent() {
const fooParams = useParams({
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
from: `/params-ps/non-nested/${useExperimentalNonNestedRoutes ? '$foo' : '$foo_'}`,
})
const fooParams = useParams({ from: `/params-ps/non-nested/$foo_` })
const routeParams = Route.useParams()

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { createFileRoute, getRouteApi, useParams } from '@tanstack/react-router'
import { useExperimentalNonNestedRoutes } from '../../tests/utils/useExperimentalNonNestedRoutes'

export const Route = createFileRoute('/posts_/$postId/edit')({
component: PostEditPage,
})

const api = getRouteApi(
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
`/${useExperimentalNonNestedRoutes ? 'posts' : 'posts_'}/$postId/edit`,
)
const api = getRouteApi(`/posts_/$postId/edit`)

function PostEditPage() {
const paramsViaApi = api.useParams()
const paramsViaHook = useParams({
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
from: `/${useExperimentalNonNestedRoutes ? 'posts' : 'posts_'}/$postId/edit`,
})
const paramsViaHook = useParams({ from: `/posts_/$postId/edit` })

const paramsViaRouteHook = Route.useParams()

Expand Down
3 changes: 2 additions & 1 deletion e2e/react-router/basic-file-based/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ testCases.forEach(({ description, testId }) => {

test('navigating to an unnested route', async ({ page }) => {
const postId = 'hello-world'
page.goto(`/posts/${postId}/edit`)
await page.goto(`/posts/${postId}/edit`)
await page.waitForURL(`/posts/${postId}/edit`)
await expect(page.getByTestId('params-via-hook')).toContainText(postId)
await expect(page.getByTestId('params-via-route-hook')).toContainText(postId)
await expect(page.getByTestId('params-via-route-api')).toContainText(postId)
Expand Down

This file was deleted.

3 changes: 0 additions & 3 deletions e2e/react-router/basic-file-based/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ export default defineConfig({
plugins: [
tanstackRouter({
target: 'react',
experimental: {
nonNestedRoutes: process.env.MODE === 'nonnested',
},
}),
react(),
],
Expand Down
9 changes: 2 additions & 7 deletions e2e/solid-router/basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:nonnested": "MODE=nonnested VITE_MODE=nonnested vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"build:nonnested": "MODE=nonnested VITE_MODE=nonnested vite build && tsc --noEmit",
"preview": "vite preview",
"preview:nonnested": "MODE=nonnested VITE_MODE=nonnested vite preview",
"start": "vite",
"start:nonnested": "MODE=nonnested VITE_MODE=nonnested vite",
"test:e2e": "pnpm run test:e2e:nonnested && pnpm run test:e2e:default",
"test:e2e:default": "rm -rf port*.txt; playwright test --project=chromium",
"test:e2e:nonnested": "rm -rf port*.txt; MODE=nonnested playwright test --project=chromium"
"test:e2e": "pnpm run test:e2e:default",
"test:e2e:default": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.15",
Expand Down
7 changes: 1 addition & 6 deletions e2e/solid-router/basic-file-based/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import {
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }
import { useExperimentalNonNestedRoutes } from './tests/utils/useExperimentalNonNestedRoutes'

const PORT = await getTestServerPort(packageJson.name)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
const experimentalNonNestedPathsModeCommand = `pnpm build:nonnested && pnpm preview:nonnested --port ${PORT}`
const defaultCommand = `pnpm build && pnpm preview --port ${PORT}`
const command = useExperimentalNonNestedRoutes
? experimentalNonNestedPathsModeCommand
: defaultCommand
const command = `pnpm build && pnpm preview --port ${PORT}`

console.info('Running with mode: ', process.env.MODE || 'default')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { createFileRoute, useParams } from '@tanstack/solid-router'
import { useExperimentalNonNestedRoutes } from '../../../../../tests/utils/useExperimentalNonNestedRoutes'

export const Route = createFileRoute('/params-ps/non-nested/$foo_/$bar')({
component: RouteComponent,
})

function RouteComponent() {
const fooParams = useParams({
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
from: `/params-ps/non-nested/${useExperimentalNonNestedRoutes ? '$foo' : '$foo_'}`,
})
const fooParams = useParams({ from: `/params-ps/non-nested/$foo_` })
const routeParams = Route.useParams()

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { createFileRoute, getRouteApi, useParams } from '@tanstack/solid-router'
import { useExperimentalNonNestedRoutes } from '../../tests/utils/useExperimentalNonNestedRoutes'

export const Route = createFileRoute('/posts_/$postId/edit')({
component: PostEditPage,
})

const api = getRouteApi(
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
`/${useExperimentalNonNestedRoutes ? 'posts' : 'posts_'}/$postId/edit`,
)
const api = getRouteApi(`/posts_/$postId/edit`)

function PostEditPage() {
const paramsViaApi = api.useParams()
const paramsViaHook = useParams({
// @ts-expect-error path is updated with new Experimental Non Nested Paths to not include the trailing underscore
from: `/${useExperimentalNonNestedRoutes ? 'posts' : 'posts_'}/$postId/edit`,
})
const paramsViaHook = useParams({ from: `/posts_/$postId/edit` })
const paramsViaRouteHook = Route.useParams()

return (
Expand Down

This file was deleted.

3 changes: 0 additions & 3 deletions e2e/solid-router/basic-file-based/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ export default defineConfig({
plugins: [
tanstackRouter({
target: 'solid',
experimental: {
nonNestedRoutes: process.env.MODE === 'nonnested',
},
}),
solid(),
],
Expand Down
2 changes: 0 additions & 2 deletions packages/router-generator/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ export const configSchema = baseConfigSchema.extend({
.object({
// TODO: This has been made stable and is now "autoCodeSplitting". Remove in next major version.
enableCodeSplitting: z.boolean().optional(),
// TODO: This resolves issues with non-nested paths in file-based routing. To be made default in next major version.
nonNestedRoutes: z.boolean().optional(),
})
.optional(),
plugins: z.array(z.custom<GeneratorPlugin>()).optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export async function getRouteNodes(
| 'disableLogging'
| 'routeToken'
| 'indexToken'
| 'experimental'
>,
root: string,
): Promise<GetRouteNodesResult> {
Expand Down Expand Up @@ -135,8 +134,7 @@ export async function getRouteNodes(
const {
routePath: initialRoutePath,
originalRoutePath: initialOriginalRoutePath,
isExperimentalNonNestedRoute,
} = determineInitialRoutePath(filePathNoExt, config)
} = determineInitialRoutePath(filePathNoExt)

let routePath = initialRoutePath
let originalRoutePath = initialOriginalRoutePath
Expand Down Expand Up @@ -228,7 +226,6 @@ export async function getRouteNodes(
routePath,
variableName,
_fsRouteType: routeType,
_isExperimentalNonNestedRoute: isExperimentalNonNestedRoute,
originalRoutePath,
})
}
Expand Down
27 changes: 4 additions & 23 deletions packages/router-generator/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
removeGroups,
removeLastSegmentFromPath,
removeLayoutSegments,
removeLeadingUnderscores,
removeTrailingSlash,
removeUnderscores,
replaceBackslash,
Expand Down Expand Up @@ -822,11 +821,8 @@ export class Generator {
let fileRoutesByFullPath = ''

if (!config.disableTypes) {
const routeNodesByFullPath = createRouteNodesByFullPath(
acc.routeNodes,
config,
)
const routeNodesByTo = createRouteNodesByTo(acc.routeNodes, config)
const routeNodesByFullPath = createRouteNodesByFullPath(acc.routeNodes)
const routeNodesByTo = createRouteNodesByTo(acc.routeNodes)
const routeNodesById = createRouteNodesById(acc.routeNodes)

fileRoutesByFullPath = [
Expand Down Expand Up @@ -1360,15 +1356,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
prefixMap: RoutePrefixMap,
config?: Config,
) {
const useExperimentalNonNestedRoutes =
config?.experimental?.nonNestedRoutes ?? false

const parentRoute = hasParentRoute(
prefixMap,
node,
node.routePath,
node.originalRoutePath,
)
const parentRoute = hasParentRoute(prefixMap, node, node.routePath)

if (parentRoute) node.parent = parentRoute

Expand All @@ -1383,15 +1371,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
lastRouteSegment.startsWith('_') ||
split.every((part) => this.routeGroupPatternRegex.test(part))

// with new nonNestedPaths feature we can be sure any remaining trailing underscores are escaped and should remain
// TODO with new major we can remove check and only remove leading underscores
node.cleanedPath = removeGroups(
(useExperimentalNonNestedRoutes
? removeLeadingUnderscores(
removeLayoutSegments(node.path ?? ''),
config?.routeToken ?? '',
)
: removeUnderscores(removeLayoutSegments(node.path))) ?? '',
removeUnderscores(removeLayoutSegments(node.path)) ?? '',
)

if (
Expand Down
1 change: 0 additions & 1 deletion packages/router-generator/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type RouteNode = {
children?: Array<RouteNode>
parent?: RouteNode
createFileRouteProps?: Set<string>
_isExperimentalNonNestedRoute?: boolean
}

export interface GetRouteNodesResult {
Expand Down
Loading
Loading