TanStackRouter v1
自动
框架
版本

Router Context

TanStack Router 的路由 context 是一个非常强大的工具,可用于依赖注入以及许多其他用途。顾名思义,路由 context 通过路由器并向下传递到每个匹配的路由。在层次结构中的每个路由上,都可以修改或添加到 context。以下是一些您可以在实践中使用路由 context 的方式

  • 依赖注入
    • 您可以提供依赖项(例如,加载器函数、数据获取客户端、mutation 服务),路由和所有子路由都可以访问和使用这些依赖项,而无需直接导入或创建。
  • 面包屑导航
    • 虽然每个路由的主要 context 对象在下降时会合并,但每个路由的唯一 context 也被存储,从而可以将面包屑导航或方法附加到每个路由的 context。
  • 动态 meta 标签管理
    • 您可以将 meta 标签附加到每个路由的 context,然后使用 meta 标签管理器在用户浏览站点时动态更新页面上的 meta 标签。

这些只是路由 context 的建议用途。您可以将其用于任何您想要的目的!

类型化的 Router Context

与所有其他内容一样,根路由 context 是严格类型化的。此类型可以通过任何路由的 beforeLoad 选项进行扩充,因为它会向下合并到路由匹配树中。要约束根路由 context 的类型,您必须使用 createRootRouteWithContext<YourContextTypeHere>()(routeOptions) 函数来创建新的路由器 context,而不是使用 createRootRoute() 函数来创建根路由。这是一个示例

tsx
import {
  createRootRouteWithContext,
  createRouter,
} from '@tanstack/react-router'

interface MyRouterContext {
  user: User
}

// Use the routerContext to create your root route
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ...
])

// Use the routerContext to create your router
const router = createRouter({
  routeTree,
})
import {
  createRootRouteWithContext,
  createRouter,
} from '@tanstack/react-router'

interface MyRouterContext {
  user: User
}

// Use the routerContext to create your root route
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ...
])

// Use the routerContext to create your router
const router = createRouter({
  routeTree,
})

传递初始 Router Context

路由 context 在实例化时传递给路由器。您可以通过 context 选项将初始路由 context 传递给路由器

提示

如果您的 context 具有任何必需的属性,如果您未在初始路由器 context 中传递它们,您将看到 TypeScript 错误。如果您的所有 context 属性都是可选的,您将不会看到 TypeScript 错误,并且传递 context 将是可选的。如果您未传递路由器 context,则默认为 {}

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

// Use the routerContext you created to create your router
const router = createRouter({
  routeTree,
  context: {
    user: {
      id: '123',
      name: 'John Doe',
    },
  },
})
import { createRouter } from '@tanstack/react-router'

// Use the routerContext you created to create your router
const router = createRouter({
  routeTree,
  context: {
    user: {
      id: '123',
      name: 'John Doe',
    },
  },
})

使 Router Context 失效

如果您需要使传递到路由器的 context 状态失效,您可以调用 invalidate 方法来告诉路由器重新计算 context。当您需要更新 context 状态并让路由器为所有路由重新计算 context 时,这非常有用。

tsx
function useAuth() {
  const router = useRouter()
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setUser(user)
      router.invalidate()
    })

    return unsubscribe
  }, [])

  return user
}
function useAuth() {
  const router = useRouter()
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setUser(user)
      router.invalidate()
    })

    return unsubscribe
  }, [])

  return user
}

使用 Router Context

定义路由器 context 类型后,您可以在路由定义中使用它

tsx
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: ({ context }) => fetchTodosByUserId(context.user.id),
})
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: ({ context }) => fetchTodosByUserId(context.user.id),
})

您甚至可以注入数据获取和 mutation 实现本身!实际上,强烈建议这样做 😜

让我们尝试使用一个简单的函数来获取一些 todos

tsx
const fetchTodosByUserId = async ({ userId }) => {
  const response = await fetch(`/api/todos?userId=${userId}`)
  const data = await response.json()
  return data
}

const router = createRouter({
  routeTree: rootRoute,
  context: {
    userId: '123',
    fetchTodosByUserId,
  },
})
const fetchTodosByUserId = async ({ userId }) => {
  const response = await fetch(`/api/todos?userId=${userId}`)
  const data = await response.json()
  return data
}

const router = createRouter({
  routeTree: rootRoute,
  context: {
    userId: '123',
    fetchTodosByUserId,
  },
})

然后,在您的路由中

tsx
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: ({ context }) => context.fetchTodosByUserId(context.userId),
})
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: ({ context }) => context.fetchTodosByUserId(context.userId),
})

外部数据获取库怎么样?

tsx
import {
  createRootRouteWithContext,
  createRouter,
} from '@tanstack/react-router'

interface MyRouterContext {
  queryClient: QueryClient
}

const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})

const queryClient = new QueryClient()

const router = createRouter({
  routeTree: rootRoute,
  context: {
    queryClient,
  },
})
import {
  createRootRouteWithContext,
  createRouter,
} from '@tanstack/react-router'

interface MyRouterContext {
  queryClient: QueryClient
}

const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})

const queryClient = new QueryClient()

const router = createRouter({
  routeTree: rootRoute,
  context: {
    queryClient,
  },
})

然后,在您的路由中

tsx
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: async ({ context }) => {
    await context.queryClient.ensureQueryData({
      queryKey: ['todos', { userId: user.id }],
      queryFn: fetchTodos,
    })
  },
})
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
  component: Todos,
  loader: async ({ context }) => {
    await context.queryClient.ensureQueryData({
      queryKey: ['todos', { userId: user.id }],
      queryFn: fetchTodos,
    })
  },
})

使用 React Context/Hooks 怎么样?

当尝试在路由的 beforeLoadloader 函数中使用 React Context 或 Hooks 时,重要的是要记住 React 的 Hooks 规则。您不能在非 React 函数中使用 hooks,因此您不能在 beforeLoadloader 函数中使用 hooks。

那么,我们如何在路由的 beforeLoadloader 函数中使用 React Context 或 Hooks? 我们可以使用路由器 context 将 React Context 或 Hooks 向下传递到路由的 beforeLoadloader 函数。

让我们看一下一个示例的设置,在该示例中,我们将 useNetworkStrength hook 传递到路由的 loader 函数

  • src/routes/\_\_root.tsx
tsx
// First, make sure the context for the root route is typed
import { createRootRouteWithContext } from '@tanstack/react-router'
import { useNetworkStrength } from '@/hooks/useNetworkStrength'

interface MyRouterContext {
  networkStrength: ReturnType<typeof useNetworkStrength>
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})
// First, make sure the context for the root route is typed
import { createRootRouteWithContext } from '@tanstack/react-router'
import { useNetworkStrength } from '@/hooks/useNetworkStrength'

interface MyRouterContext {
  networkStrength: ReturnType<typeof useNetworkStrength>
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})

在此示例中,我们将在使用 <RouterProvider /> 渲染路由器之前实例化 hook。这样,hook 将在 React 领域中调用,因此遵循 Hooks 规则。

  • src/router.tsx
tsx
import { createRouter } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

export const router = createRouter({
  routeTree,
  context: {
    networkStrength: undefined!, // We'll set this in React-land
  },
})
import { createRouter } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

export const router = createRouter({
  routeTree,
  context: {
    networkStrength: undefined!, // We'll set this in React-land
  },
})
  • src/main.tsx
tsx
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'

import { useNetworkStrength } from '@/hooks/useNetworkStrength'

function App() {
  const networkStrength = useNetworkStrength()
  // Inject the returned value from the hook into the router context
  return <RouterProvider router={router} context={{ networkStrength }} />
}

// ...
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'

import { useNetworkStrength } from '@/hooks/useNetworkStrength'

function App() {
  const networkStrength = useNetworkStrength()
  // Inject the returned value from the hook into the router context
  return <RouterProvider router={router} context={{ networkStrength }} />
}

// ...

因此,现在在我们的路由的 loader 函数中,我们可以从路由器 context 访问 networkStrength hook

  • src/routes/posts.tsx
tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  component: Posts,
  loader: ({ context }) => {
    if (context.networkStrength === 'STRONG') {
      // Do something
    }
  },
})
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  component: Posts,
  loader: ({ context }) => {
    if (context.networkStrength === 'STRONG') {
      // Do something
    }
  },
})

修改 Router Context

路由器 context 在路由树中向下传递,并在每个路由处合并。这意味着您可以在每个路由处修改 context,并且修改将对所有子路由可用。这是一个示例

  • src/routes/\_\_root.tsx
tsx
import { createRootRouteWithContext } from '@tanstack/react-router'

interface MyRouterContext {
  foo: boolean
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})
import { createRootRouteWithContext } from '@tanstack/react-router'

interface MyRouterContext {
  foo: boolean
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: App,
})
  • src/router.tsx
tsx
import { createRouter } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

const router = createRouter({
  routeTree,
  context: {
    foo: true,
  },
})
import { createRouter } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

const router = createRouter({
  routeTree,
  context: {
    foo: true,
  },
})
  • src/routes/todos.tsx
tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/todos')({
  component: Todos,
  beforeLoad: () => {
    return {
      bar: true,
    }
  },
  loader: ({ context }) => {
    context.foo // true
    context.bar // true
  },
})
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/todos')({
  component: Todos,
  beforeLoad: () => {
    return {
      bar: true,
    }
  },
  loader: ({ context }) => {
    context.foo // true
    context.bar // true
  },
})

处理累积的路由 Context

Context,特别是隔离的路由 context 对象,使累积和处理所有匹配路由的路由 context 对象变得非常简单。这是一个示例,我们在其中使用所有匹配的路由 context 来生成面包屑导航轨迹

tsx
// src/routes/__root.tsx
export const Route = createRootRoute({
  component: () => {
    const matches = useRouterState({ select: (s) => s.matches })

    const breadcrumbs = matches
      .filter((match) => match.context.getTitle)
      .map(({ pathname, context }) => {
        return {
          title: context.getTitle(),
          path: pathname,
        }
      })

    // ...
  },
})
// src/routes/__root.tsx
export const Route = createRootRoute({
  component: () => {
    const matches = useRouterState({ select: (s) => s.matches })

    const breadcrumbs = matches
      .filter((match) => match.context.getTitle)
      .map(({ pathname, context }) => {
        return {
          title: context.getTitle(),
          path: pathname,
        }
      })

    // ...
  },
})

使用相同的路由 context,我们还可以为页面的 <head> 生成标题标签

tsx
// src/routes/__root.tsx
export const Route = createRootRoute({
  component: () => {
    const matches = useRouterState({ select: (s) => s.matches })

    const matchWithTitle = [...matches]
      .reverse()
      .find((d) => d.context.getTitle)

    const title = matchWithTitle?.context.getTitle() || 'My App'

    return (
      <html>
        <head>
          <title>{title}</title>
        </head>
        <body>{/* ... */}</body>
      </html>
    )
  },
})
// src/routes/__root.tsx
export const Route = createRootRoute({
  component: () => {
    const matches = useRouterState({ select: (s) => s.matches })

    const matchWithTitle = [...matches]
      .reverse()
      .find((d) => d.context.getTitle)

    const title = matchWithTitle?.context.getTitle() || 'My App'

    return (
      <html>
        <head>
          <title>{title}</title>
        </head>
        <body>{/* ... */}</body>
      </html>
    )
  },
})
订阅 Bytes

您的每周 JavaScript 新闻速递。每周一免费发送给超过 100,000 名开发者。

Bytes

绝无垃圾邮件。随时取消订阅。