学习基础知识

本指南将帮助您了解 TanStack Start 的基本原理,无论您如何设置项目。

依赖项

TanStack Start 由 ViteTanStack Router 提供支持。

  • TanStack Router:一个用于构建 Web 应用程序的路由器。
  • Vite:一个用于打包应用程序的构建工具。

一切都“始于”路由器

这是将决定 Start 中使用的 TanStack Router 行为的文件。在这里,您可以配置从默认的预加载功能缓存陈旧性的所有内容。

tsx
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}
  • 请注意 scrollRestoration 属性。这用于在路由之间导航时恢复页面的滚动位置。

路由生成

当您第一次运行 TanStack Start(通过 npm run devnpm run start)时,会生成 routeTree.gen.ts 文件。此文件包含生成的路由树和一些 TS 工具,使 TanStack Start 具有完整的类型安全。

服务器入口点(可选)

注意

服务器入口点是开箱即用的可选功能。如果未提供,TanStack Start 将自动为您处理服务器入口点,并使用以下内容作为默认值。

这是通过 src/server.ts 文件完成的

tsx
// src/server.ts
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'

export default createStartHandler({
  createRouter,
})(defaultStreamHandler)
// src/server.ts
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'

export default createStartHandler({
  createRouter,
})(defaultStreamHandler)

无论我们是静态生成应用程序还是动态提供应用程序,server.ts 文件都是执行所有 SSR 相关工作的入口点。

  • 为每个请求创建一个新的路由器非常重要。这确保了路由器处理的任何数据对于该请求都是唯一的。
  • defaultStreamHandler 函数用于将我们的应用程序渲染到流中,从而使我们能够利用流式 HTML 到客户端。(这是默认处理程序,但您也可以使用其他处理程序,例如 defaultRenderHandler,甚至可以构建自己的处理程序)

客户端入口点(可选)

注意

客户端入口点是开箱即用的可选功能。如果未提供,TanStack Start 将自动为您处理客户端入口点,并使用以下内容作为默认值。

将 HTML 发送到客户端只成功了一半。一旦到达那里,我们需要在路由解析到客户端后,对客户端 JavaScript 进行水合。我们通过使用 StartClient 组件对应用程序的根进行水合来实现这一点

tsx
// src/client.tsx
import { StartClient } from '@tanstack/react-start'
import { StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { createRouter } from './router'

const router = createRouter()

hydrateRoot(
  document,
  <StrictMode>
    <StartClient router={router} />
  </StrictMode>,
)
// src/client.tsx
import { StartClient } from '@tanstack/react-start'
import { StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { createRouter } from './router'

const router = createRouter()

hydrateRoot(
  document,
  <StrictMode>
    <StartClient router={router} />
  </StrictMode>,
)

这使我们能够在用户的初始服务器请求完成后启动客户端路由。

应用程序的根目录

除了客户端入口点(默认可选)之外,应用程序的 __root 路由是您应用程序的入口点。此文件中的代码将包装应用程序中的所有其他路由,包括您的主页。它的行为就像您的整个应用程序的无路径布局路由。

因为它总是被渲染,所以它是构建应用程序外壳并处理任何全局逻辑的理想场所。

tsx
// src/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/react-router'
import type { ReactNode } from 'react'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
      {
        title: 'TanStack Start Starter',
      },
    ],
  }),
  component: RootComponent,
})

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  )
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  )
}
// src/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/react-router'
import type { ReactNode } from 'react'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
      {
        title: 'TanStack Start Starter',
      },
    ],
  }),
  component: RootComponent,
})

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  )
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  )
}
  • 随着我们推出 SPA 模式,此布局可能会在未来发生变化,该模式允许根路由渲染 SPA 外壳,而不包含任何页面特定的内容。
  • 请注意 Scripts 组件。它用于加载应用程序的所有客户端 JavaScript。

路由

路由是 TanStack Router 的一项重要功能,在路由指南中已详细介绍。总结如下:

  • 路由使用 createFileRoute 函数定义。
  • 路由会自动进行代码分割和懒加载。
  • 关键数据获取由路由的加载器协调
  • 还有更多!
tsx
// src/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((d: number) => d)
  .handler(async ({ data }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + data}`)
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const router = useRouter()
  const state = Route.useLoaderData()

  return (
    <button
      type="button"
      onClick={() => {
        updateCount({ data: 1 }).then(() => {
          router.invalidate()
        })
      }}
    >
      Add 1 to {state}?
    </button>
  )
}
// src/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((d: number) => d)
  .handler(async ({ data }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + data}`)
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const router = useRouter()
  const state = Route.useLoaderData()

  return (
    <button
      type="button"
      onClick={() => {
        updateCount({ data: 1 }).then(() => {
          router.invalidate()
        })
      }}
    >
      Add 1 to {state}?
    </button>
  )
}

TanStack Start 完全基于 TanStack Router 构建,因此 TanStack Router 的所有导航功能都可供您使用。总结如下:

  • 使用 Link 组件导航到新路由。
  • 使用 useNavigate Hook 以命令式导航。
  • 在应用程序中的任何位置使用 useRouter Hook 访问路由器实例并执行失效操作。
  • 每个返回状态的路由器 Hook 都是响应式的,这意味着当相应的状态发生变化时,它会自动重新运行。

这是一个快速示例,演示如何使用 Link 组件导航到新路由

tsx
import { Link } from '@tanstack/react-router'

function Home() {
  return <Link to="/about">About</Link>
}
import { Link } from '@tanstack/react-router'

function Home() {
  return <Link to="/about">About</Link>
}

有关导航的更深入信息,请查阅导航指南

服务器函数 (RPCs)

您可能已经注意到我们上面使用 createServerFn 创建的服务器函数。这是 TanStack 最强大的功能之一,它允许您创建可以从服务器(SSR 期间)和客户端调用的服务器端函数!

以下是服务器函数工作原理的快速概述

  • 服务器函数使用 createServerFn 函数创建。
  • 它们可以在 SSR 期间从服务器和客户端调用。
  • 它们可用于从服务器获取数据,或执行其他服务器端操作。

这是一个快速示例,演示如何使用服务器函数从服务器获取并返回数据

tsx
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import { z } from 'zod'

const getUserById = createServerFn({ method: 'GET' })
  // Always validate data sent to the function, here we use Zod
  .validator(z.string())
  // The handler function is where you perform the server-side logic
  .handler(async ({ data }) => {
    return db.query.users.findFirst({ where: eq(users.id, data) })
  })

// Somewhere else in your application
const user = await getUserById({ data: '1' })
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import { z } from 'zod'

const getUserById = createServerFn({ method: 'GET' })
  // Always validate data sent to the function, here we use Zod
  .validator(z.string())
  // The handler function is where you perform the server-side logic
  .handler(async ({ data }) => {
    return db.query.users.findFirst({ where: eq(users.id, data) })
  })

// Somewhere else in your application
const user = await getUserById({ data: '1' })

要了解有关服务器函数的更多信息,请查阅服务器函数指南

变更

服务器函数也可以用于在服务器上执行突变。这也使用相同的 createServerFn 函数完成,但有一个额外的要求,即您需要使客户端上受突变影响的任何数据失效。

  • 如果您只使用 TanStack Router,您可以使用 router.invalidate() 方法使所有路由器数据失效并重新获取。
  • 如果您正在使用 TanStack Query,您可以使用 queryClient.invalidateQueries() 方法来使数据失效,以及其他更具体的方法来针对特定查询。

这是一个快速示例,演示如何使用服务器函数在服务器上执行突变并使客户端上的数据失效

tsx
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { dbUpdateUser } from '...'

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
})
export type User = z.infer<typeof UserSchema>

export const updateUser = createServerFn({ method: 'POST' })
  .validator(UserSchema)
  .handler(({ data }) => dbUpdateUser(data))

// Somewhere else in your application
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { updateUser, type User } from '...'

export function useUpdateUser() {
  const router = useRouter()
  const queryClient = useQueryClient()
  const _updateUser = useServerFn(updateUser)

  return useCallback(
    async (user: User) => {
      const result = await _updateUser({ data: user })

      router.invalidate()
      queryClient.invalidateQueries({
        queryKey: ['users', 'updateUser', user.id],
      })

      return result
    },
    [router, queryClient, _updateUser],
  )
}

// Somewhere else in your application
import { useUpdateUser } from '...'

function MyComponent() {
  const updateUser = useUpdateUser()
  const onClick = useCallback(async () => {
    await updateUser({ id: '1', name: 'John' })
    console.log('Updated user')
  }, [updateUser])

  return <button onClick={onClick}>Click Me</button>
}
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { dbUpdateUser } from '...'

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
})
export type User = z.infer<typeof UserSchema>

export const updateUser = createServerFn({ method: 'POST' })
  .validator(UserSchema)
  .handler(({ data }) => dbUpdateUser(data))

// Somewhere else in your application
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { updateUser, type User } from '...'

export function useUpdateUser() {
  const router = useRouter()
  const queryClient = useQueryClient()
  const _updateUser = useServerFn(updateUser)

  return useCallback(
    async (user: User) => {
      const result = await _updateUser({ data: user })

      router.invalidate()
      queryClient.invalidateQueries({
        queryKey: ['users', 'updateUser', user.id],
      })

      return result
    },
    [router, queryClient, _updateUser],
  )
}

// Somewhere else in your application
import { useUpdateUser } from '...'

function MyComponent() {
  const updateUser = useUpdateUser()
  const onClick = useCallback(async () => {
    await updateUser({ id: '1', name: 'John' })
    console.log('Updated user')
  }, [updateUser])

  return <button onClick={onClick}>Click Me</button>
}

要了解有关突变的更多信息,请查阅突变指南

数据加载

TanStack Router 的另一个强大功能是数据加载。它允许您在渲染之前为 SSR 获取数据并预加载路由数据。这是通过路由的 loader 函数完成的。

以下是数据加载工作原理的快速概述

  • 数据加载是使用路由的 loader 函数完成的。
  • 数据加载器是同构的,这意味着它们在服务器和客户端上都执行。
  • 要执行仅服务器逻辑,请在加载器内部调用服务器函数。
  • 与 TanStack Query 类似,数据加载器在客户端被缓存,并在数据过时时被重用,甚至在后台重新获取。

要了解有关数据加载的更多信息,请查阅数据加载指南

我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。