Published on
1337

React 中 BrowserRouter 与 useRoutes 的"时序陷阱"

Authors
  • avatar
    Name
    小辉辉
    Twitter

一个常见的报错

在 React Router v6 中,很多开发者都遇到过这样一个报错:

useRoutes() may be used only in the context of a <Router> component

明明代码里已经包裹了 <BrowserRouter>,为什么还会报这个错?

function App() {
  return (
    <BrowserRouter>
      {useRoutes(routesData)}  // ❌ 为什么这里不行?
    </BrowserRouter>
  );
}

本文将深入分析这个问题的根本原因,带你理解 React 渲染机制中一个容易被忽视的细节。


问题复现

让我们先看两段代码的对比:

❌ 错误写法:

function App() {
  return (
    <BrowserRouter>
      {useRoutes(routesData)}
    </BrowserRouter>
  );
}

✅ 正确写法:

function App() {
  return (
    <BrowserRouter>
      <AppRoutes />
    </BrowserRouter>
  );
}

function AppRoutes() {
  return useRoutes(routesData);
}

两段代码看起来几乎一模一样,为什么结果却截然不同?答案藏在 React 的渲染机制里。


React Context 的工作原理

要理解这个问题,首先需要理解 React Context 的工作机制。

Context 的基本用法

// 1. 创建 Context
const MyContext = createContext(null);

// 2. Provider 提供值
<MyContext.Provider value={{ name: 'Alice' }}>
  {children}
</MyContext.Provider>

// 3. Consumer 消费值
const value = useContext(MyContext);

关键点在于:useContext 只能在 Provider 的子组件中生效

BrowserRouter 的内部实现

BrowserRouter 本质上就是一个 Context Provider。简化版实现如下:

function BrowserRouter({ children }) {
  const [state, setState] = useState({
    location: window.location,
    action: 'POP'
  });

  return (
    <NavigationContext.Provider value={{ location: state.location }}>
      <RouteContext.Provider value={{ match: { path: '/' } }}>
        {children}
      </RouteContext.Provider>
    </NavigationContext.Provider>
  );
}

useRoutes 则是一个 Consumer,它需要读取这些 Context:

function useRoutes(routes) {
  const location = useContext(NavigationContext).location;
  const match = useContext(RouteContext).match;
  
  // 根据当前 location 匹配路由...
  return matchedElement;
}

关键问题:React 的渲染执行顺序

现在问题来了:React 组件什么时候开始渲染?

JSX 的执行时机

很多人误以为 JSX 是"同时"执行的。实际上,JSX 是按顺序执行的函数调用:

function App() {
  // 第一步:App 函数体开始执行
  console.log('1. App 开始执行');
  
  return (
    <BrowserRouter>
      <Child />
    </BrowserRouter>
  );
  
  // JSX 被转换为:
  // React.createElement(BrowserRouter, null, React.createElement(Child, null))
}

function BrowserRouter({ children }) {
  console.log('2. BrowserRouter 开始执行');
  // 创建 Context,return JSX...
}

function Child() {
  console.log('3. Child 开始执行');
  // 可以使用 Context...
}

执行顺序是:App → BrowserRouter → Child

Context 的创建时机

回到我们的问题代码:

function App() {
  return (
    <BrowserRouter>
      {useRoutes(routesData)}  // 这里发生了什么?
    </BrowserRouter>
  );
}

这段代码会被 Babel 转换为:

function App() {
  // useRoutes 在 BrowserRouter 渲染之前就被调用了!
  const routesResult = useRoutes(routesData);
  
  return React.createElement(
    BrowserRouter,
    null,
    routesResult
  );
}

看到了吗?useRoutes() 是在 App 函数体内直接调用的,而此时 BrowserRouter 还没有开始渲染,Context 自然也就不存在。


图解渲染流程

错误写法的执行流程

┌─────────────────────────────────────────────────────────┐
App() 开始执行                                           │
│                                                         │
│   ├─ useRoutes() 被调用                                  │
│   │   └─ useContext(NavigationContext)│   │       └─ ❌ Context 不存在,报错!                   │
│   │                                                     │
│   └─ (BrowserRouter 根本没有机会渲染)                  │
│                                                         │
└─────────────────────────────────────────────────────────┘

正确写法的执行流程

┌─────────────────────────────────────────────────────────┐
App() 开始执行                                           │
│                                                         │
│   └─ return <BrowserRouter><AppRoutes /></BrowserRouter>│                                                         │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
BrowserRouter() 开始执行                                 │
│                                                         │
│   ├─ 创建 NavigationContext│   ├─ 创建 RouteContext│   └─ return <Context.Provider><AppRoutes /></Provider>│                                                         │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
AppRoutes() 开始执行                                     │
│                                                         │
│   └─ useRoutes()│       └─ useContext(NavigationContext)│           └─ ✅ Context 存在,正常工作!                │
│                                                         │
└─────────────────────────────────────────────────────────┘

深入理解:为什么 JSX 写法看起来没问题?

这是最容易让人困惑的地方。很多人认为这样写是"并行"的:

<BrowserRouter>
  {useRoutes(routesData)}
</BrowserRouter>

看起来 useRoutes 像是在 BrowserRouter "内部",但实际上:

// 等价于
React.createElement(
  BrowserRouter,
  null,
  useRoutes(routesData)  // 参数先被求值!
)

JavaScript 函数的参数会在函数调用之前被求值。这是语言的特性,与 React 无关。

一个简单的类比

function createHouse(occupant) {
  const house = { built: true };
  console.log('房子建好了');
  return { house, occupant };
}

// 问题写法
createHouse(enterHouse());  // ❌ enterHouse 在房子建好之前就被调用了

// 正确写法
createHouse(() => enterHouse());  // ✅ 延迟执行

解决方案汇总

方案一:创建子组件(推荐)

function App() {
  return (
    <BrowserRouter>
      <AppRoutes />
    </BrowserRouter>
  );
}

function AppRoutes() {
  return useRoutes(routesData);
}

方案二:使用 RouterProvider

React Router v6.4+ 推荐的数据路由器模式:

const router = createBrowserRouter(routesData);

function App() {
  return <RouterProvider router={router} />;
}

这种方式完全避免了 Context 的问题,因为 RouterProvider 内部会自动处理所有依赖。

方案三:内联函数组件

function App() {
  const Routes = () => useRoutes(routesData);
  
  return (
    <BrowserRouter>
      <Routes />
    </BrowserRouter>
  );
}

虽然可以工作,但不推荐,因为每次渲染都会创建新组件。


总结

问题原因
useRoutesBrowserRouter 内报错JSX 参数在父组件函数体内被求值,Context 尚未创建
创建子组件后正常子组件在 Provider 渲染后才开始执行,Context 已存在

这个问题的本质是 JavaScript 函数参数的求值顺序 + React Context 的依赖关系

理解这个原理后,你会发现 React 中很多类似的问题都遵循同样的模式:

  • useContext 必须在 Provider 子组件中调用
  • useStore 必须在 Provider 子组件中调用
  • 自定义 hooks 如果依赖 Context,也要遵循同样的规则

记住:Provider 的 "子组件" 不是指 JSX 结构上的位置,而是指渲染时序上的先后。


延伸阅读