Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

路由上下文 (Router Context)

TanStack Router 的路由上下文是一个非常强大的工具,可用于依赖注入等多种用途。顾名思义,路由上下文贯穿于路由器,并向下传递给每一个匹配的路由。在层级结构中的每个路由上,都可以修改或增加上下文内容。以下是路由上下文的一些实际用途:

这些只是建议的用途,你可以根据需求随心所欲地使用路由上下文!

类型化路由上下文

与其它功能一样,根路由上下文是严格类型化的。该类型可以通过任何路由的 beforeLoad 选项在路由匹配树向下合并时进行增强。要约束根路由上下文的类型,你必须使用 createRootRouteWithContext<YourContextTypeHere>()(routeOptions) 函数来创建根路由,而不是使用普通的 createRootRoute()。示例如下:

import {
  createRootRouteWithContext,
  createRouter,
} from "@tanstack/react-router";

interface MyRouterContext {
  user: User;
}

// 使用带有上下文定义的 routerContext 来创建你的根路由
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: App,
});

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

// 使用路由树创建路由器
const router = createRouter({
  routeTree,
});

传递初始路由上下文

路由上下文在实例化时传递给路由器。你可以通过 context 选项传递初始上下文:

import { createRouter } from "@tanstack/react-router";

// 使用你创建的路由树和初始上下文来创建路由器
const router = createRouter({
  routeTree,
  context: {
    user: {
      id: "123",
      name: "John Doe",
    },
  },
});

刷新路由上下文 (Invalidating)

如果你需要刷新传递给路由器的上下文状态,可以调用 invalidate 方法告知路由器重新计算上下文。当你更新了上下文状态并希望路由器为所有路由重新计算上下文时,这非常有用。

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;
}

使用路由上下文

定义好路由上下文类型后,你就可以在路由定义中使用它:

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

你甚至可以将数据获取和 Mutation 的实现逻辑注入其中!事实上,我们强烈建议这样做 😜。

让我们尝试注入一个简单的获取 Todo 的函数:

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,
  },
});

然后,在你的路由中:

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

配合外部数据获取库使用?

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,
  },
});

然后,在你的路由中:

// 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 规则 (Rules of Hooks)。你不能在非 React 函数中使用 Hook,因此无法直接在 beforeLoadloader 中调用它们。

那么,该如何变通呢?我们可以利用路由上下文 (Router Context) 将 React Context 或 Hooks 的状态传递给路由函数

让我们来看一个 setup 示例,我们将 useNetworkStrength Hook 的返回值传递给路由的 loader 函数:

// 首先,确保根路由的上下文已定义类型
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 规则。

import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

export const router = createRouter({
  routeTree,
  context: {
    // 初始设为 undefined!(非空断言),稍后在 React 组件中设置
    networkStrength: undefined!,
  },
});

接下来,我们在 App 组件中调用 useNetworkStrength Hook,并通过 <RouterProvider />context 属性将返回值注入到路由上下文中:

import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";
import { useNetworkStrength } from "@/hooks/useNetworkStrength";

function App() {
  const networkStrength = useNetworkStrength();

  // 将 Hook 返回的值注入到路由上下文中
  return <RouterProvider router={router} context={{ networkStrength }} />;
}

// ...

现在,在路由的 loader 函数中,我们就可以从路由上下文中访问 networkStrength 了:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts")({
  component: Posts,
  loader: ({ context }) => {
    if (context.networkStrength === "STRONG") {
      // 执行相关操作
    }
  },
});

修改路由上下文

路由上下文会沿着路由树向下传递,并在每个路由节点进行合并。这意味着你可以在每个路由节点修改上下文,这些修改将对所有子路由可见。示例如下:

import { createRootRouteWithContext } from "@tanstack/react-router";

interface MyRouterContext {
  foo: boolean;
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: App,
});
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

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

export const Route = createFileRoute("/todos")({
  component: Todos,
  beforeLoad: () => {
    // 这里返回的对象会被合并到上下文中
    return {
      bar: true,
    };
  },
  loader: ({ context }) => {
    console.log(context.foo); // true
    console.log(context.bar); // true
  },
});

处理累积的路由上下文

上下文(特别是独立的路由 context 对象)使得累积和处理所有匹配路由的上下文变得非常简单。以下是一个使用所有匹配路由的上下文来生成面包屑导航(Breadcrumbs)的示例:

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,
        };
      });

    // ... 渲染面包屑 UI
  },
});

利用同样的逻辑,我们还可以为页面的 <head> 生成 title 标签:

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>
    );
  },
});