Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/TanStack/router
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 7, 2024
2 parents 45fc0ef + cebb221 commit 3508300
Show file tree
Hide file tree
Showing 205 changed files with 2,954 additions and 1,804 deletions.
64 changes: 61 additions & 3 deletions docs/framework/react/guide/deferred-data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ TanStack Router is designed to run loaders in parallel and wait for all of them

Deferred data loading is a pattern that allows the router to render the next location's critical data/markup while slower, non-critical route data is resolved in the background. This process works on both the client and server (via streaming) and is a great way to improve the perceived performance of your application.

If you are using a library like [TanStack Query](https://react-query.tanstack.com) or any other data fetching library, then deferred data loading works a bit differently. Skip ahead to the [Deferred Data Loading with External Libraries](#deferred-data-loading-with-external-libraries) section for more information.

## Deferred Data Loading with `defer` and `Await`

To defer slow or non-critical data, wrap an **unawaited/unresolved** promise in the `defer` function and return it anywhere in your loader response:

```tsx
// src/routes/posts.$postId.tsx

import * as React from 'react'
import { createFileRoute, defer } from '@tanstack/react-router'

Expand All @@ -40,7 +41,6 @@ In the component, deferred promises can be resolved and utilized using the `Awai

```tsx
// src/routes/posts.$postId.tsx

import * as React from 'react'
import { createFileRoute, Await } from '@tanstack/react-router'

Expand All @@ -50,7 +50,9 @@ export const Route = createFileRoute('/posts/$postId')({
})

function PostIdComponent() {
const { deferredSlowData } = Route.useLoaderData()
const { deferredSlowData, fastData } = Route.useLoaderData()

// do something with fastData

return (
<Await promise={deferredSlowData} fallback={<div>Loading...</div>}>
Expand All @@ -69,6 +71,62 @@ The `Await` component resolves the promise by triggering the nearest suspense bo

If the promise is rejected, the `Await` component will throw the serialized error, which can be caught by the nearest error boundary.

## Deferred Data Loading with External libraries

When your strategy for fetching information for the route relies on [External Data Loading](./external-data-loading.md) with an external library like [TanStack Query](https://react-query.tanstack.com), deferred data loading works a bit differently, as the library handles the data fetching and caching for you outside of TanStack Router.

So, instead of using `defer` and `Await`, you'll instead want to use the Route's `loader` to kick off the data fetching and then use the library's hooks to access the data in your components.

```tsx
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient } }) => {
// Kick off the fetching of some slower data, but do not await it
queryClient.prefetchQuery(slowDataOptions())

// Fetch and await some data that resolves quickly
await queryClient.ensureQueryData(fastDataOptions())
},
})
```

Then in your component, you can use the library's hooks to access the data:

```tsx
// src/routes/posts.$postId.tsx
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { slowDataOptions, fastDataOptions } from '~/api/query-options'

export const Route = createFileRoute('/posts/$postId')({
// ...
component: PostIdComponent,
})

function PostIdComponent() {
const fastData = useSuspenseQuery(fastDataOptions())

// do something with fastData

return (
<React.Suspense fallback={<div>Loading...</div>}>
<SlowDataComponent />
</React.Suspense>
)
}

function SlowDataComponent() {
const data = useSuspenseQuery(slowDataOptions())

return <div>{data}</div>
}
```

## Caching and Invalidation

Streamed promises follow the same lifecycle as the loader data they are associated with. They can even be preloaded!
Expand Down
56 changes: 46 additions & 10 deletions docs/framework/react/guide/virtual-file-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Virtual file routes are a powerful concept that allows you to build a route tree
Here's a quick example of using virtual file routes to map a route tree to a set of real files in your project:

```tsx
// routes.ts
import {
rootRoute,
route,
Expand All @@ -22,7 +23,7 @@ import {
physical,
} from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
index('index.tsx'),
layout('layout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
Expand All @@ -46,9 +47,28 @@ Virtual file routes can be configured either via:

## Configuration via the TanStackRouter Plugin

If you're using the `TanStackRouter` plugin for Vite/Rspack/Webpack, you can configure virtual file routes by passing a `virtualRoutesConfig` option to the plugin:
If you're using the `TanStackRouter` plugin for Vite/Rspack/Webpack, you can configure virtual file routes by passing the path of your routes file to the `virtualRoutesConfig` option when setting up the plugin:

```tsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
plugins: [
TanStackRouterVite({
virtualRouteConfig: './routes.ts',
}),
react(),
],
})
```

Or, you choose to define the virtual routes directly in the configuration:

```tsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
Expand Down Expand Up @@ -78,9 +98,10 @@ To create virtual file routes, you'll need to import the `@tanstack/virtual-file
The `rootRoute` function is used to create a virtual root route. It takes a file name and an array of children routes. Here's an example of a virtual root route:

```tsx
// routes.ts
import { rootRoute } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
// ... children routes
])
```
Expand All @@ -90,9 +111,10 @@ const virtualRouteConfig = rootRoute('root.tsx', [
The `route` function is used to create a virtual route. It takes a path, a file name, and an array of children routes. Here's an example of a virtual route:

```tsx
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
route('/about', 'about.tsx', [
// ... children routes
]),
Expand All @@ -102,9 +124,10 @@ const virtualRouteConfig = rootRoute('root.tsx', [
You can also define a virtual route without a file name. This allows to set a common path prefix for its children:

```tsx
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
route('/hello', [
route('/world', 'world.tsx'), // full path will be "/hello/world"
route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
Expand All @@ -119,17 +142,18 @@ The `index` function is used to create a virtual index route. It takes a file na
```tsx
import { index } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [index('index.tsx')])
const routes = rootRoute('root.tsx', [index('index.tsx')])
```

## Virtual Layout Route

The `layout` function is used to create a virtual layout route. It takes a file name, an array of children routes, and an optional layout ID. Here's an example of a virtual layout route:

```tsx
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
layout('layout.tsx', [
// ... children routes
]),
Expand All @@ -139,9 +163,10 @@ const virtualRouteConfig = rootRoute('root.tsx', [
You can also specify a layout ID to give the layout a unique identifier that is different from the filename:

```tsx
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
export const routes = rootRoute('root.tsx', [
layout('my-layout-id', 'layout.tsx', [
// ... children routes
]),
Expand Down Expand Up @@ -180,7 +205,8 @@ Consider the following file structure:
Let's use virtual routes to customize our route tree for everything but `posts`, then use physical virtual routes to mount the `posts` directory under the `/posts` path:

```tsx
const virtualRouteConfig = rootRoute('root.tsx', [
// routes.ts
export const routes = rootRoute('root.tsx', [
// Set up your virtual routes as normal
index('index.tsx'),
layout('layout.tsx', [
Expand Down Expand Up @@ -223,6 +249,7 @@ Let's look at the `bar` directory which contains a special file named `__virtual
`__virtual.ts` configures the virtual routes for that particular subtree of the route tree. It uses the same API as explained above, with the only difference being that no `rootRoute` is defined for that subtree:

```tsx
// routes/foo/bar/__virtual.ts
import {
defineVirtualSubtreeConfig,
index,
Expand Down Expand Up @@ -264,7 +291,16 @@ Check out the following example that starts off using File Based routing convent

## Configuration via the TanStack Router CLI

While much less common, you can also configure virtual file routes via the TanStack Router CLI by adding a `virtualRouteConfig` object to your `tsr.config.json` file and defining your virtual routes and passing the resulting JSON that is generated by calling the actual `rootRoute`/`route`/`index`/etc functions from the `@tanstack/virtual-file-routes` package:
If you're using the TanStack Router CLI, you can configure virtual file routes by defining the path to your routes file in the `tsr.config.json` file:

```json
// tsr.config.json
{
"virtualRouteConfig": "./routes.ts"
}
```

Or you can define the virtual routes directly in the configuration, while much less common allows you to configure them via the TanStack Router CLI by adding a `virtualRouteConfig` object to your `tsr.config.json` file and defining your virtual routes and passing the resulting JSON that is generated by calling the actual `rootRoute`/`route`/`index`/etc functions from the `@tanstack/virtual-file-routes` package:

```json
// tsr.config.json
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-router/basic-esbuild-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "esbuild src/main.tsx --serve=3090 --bundle --outfile=dist/main.js --watch --servedir=.",
"dev": "esbuild src/main.tsx --serve=5601 --bundle --outfile=dist/main.js --watch --servedir=.",
"build": "esbuild src/main.tsx --bundle --outfile=dist/main.js",
"serve": "esbuild src/main.tsx --bundle --outfile=dist/main.js --servedir=.",
"start": "dev",
Expand Down
11 changes: 8 additions & 3 deletions e2e/react-router/basic-esbuild-file-based/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { defineConfig, devices } from '@playwright/test'
import { derivePort } from '../../utils.js'
import packageJson from './package.json' with { type: 'json' }

const PORT = derivePort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3090/',
baseURL,
},

webServer: {
command: 'pnpm run dev',
url: 'http://localhost:3090',
command: `pnpm run build && pnpm run serve --serve=${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},
Expand Down
10 changes: 8 additions & 2 deletions e2e/react-router/basic-esbuild-file-based/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"skipLibCheck": true
}
"target": "ESNext",
"moduleResolution": "Bundler",
"module": "ESNext",
"skipLibCheck": true,
"resolveJsonModule": true,
"allowJs": true
},
"exclude": ["node_modules", "dist"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.3.4",
"get-port-please": "^3.1.2",
"terminate": "^2.8.0",
"vite": "^5.4.11",
"wait-port": "^1.1.0"
"vite": "^6.0.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { defineConfig, devices } from '@playwright/test'
import { derivePort } from '../../utils.js'
import packageJson from './package.json' with { type: 'json' }

const PORT = derivePort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

// use: {
// /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3001/',
// },
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
},

// webServer: {
// command: 'pnpm run dev',
// url: 'http://localhost:3001',
// reuseExistingServer: !process.env.CI,
// stdout: 'pipe',
// },
webServer: {
command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},

projects: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { expect } from '@playwright/test'
import { test } from './utils'
import { expect, test } from '@playwright/test'

test.beforeEach(async ({ page, setupApp }) => {
await page.goto(setupApp.ADDR + '/')
})

test.afterEach(async ({ setupApp }) => {
await setupApp.killProcess()
test.beforeEach(async ({ page }) => {
await page.goto('/')
})

test('Navigating to a post page', async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { expect } from '@playwright/test'
import { test } from './utils'
import { expect, test } from '@playwright/test'

test.beforeEach(async ({ page, setupApp }) => {
await page.goto(setupApp.ADDR + '/')
})

test.afterEach(async ({ setupApp }) => {
await setupApp.killProcess()
test.beforeEach(async ({ page }) => {
await page.goto('/')
})

test('hovering a link with preload=intent to a route without a loader should preload route', async ({
Expand Down
Loading

0 comments on commit 3508300

Please sign in to comment.