## 1 引言

[React Router v6](https://github.com/ReactTraining/react-router) alpha 版本发布了，本周通过 [A Sneak Peek at React Router v6](https://alligator.io/react/react-router-v6/) 这篇文章分析一下带来的改变。

## 2 概述

### <Switch> 更名为 <Routes>

一个不痛不痒的改动，使 API 命名更加规范。

```jsx
// v5
import { BrowserRouter, Switch, Route } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/profile">
          <Profile />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}
```

在 React Router v6 版本里，直接使用 `Routes` 替代 `Switch`：

```jsx
// v6
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="profile/*" element={<Profile />} />
      </Routes>
    </BrowserRouter>
  );
}
```

### <Route> 升级

在 v5 版本里，想要给组件传参数是不太直观的，需要利用 RenderProps 的方式透传 `routeProps`：

```jsx
import Profile from './Profile';

// v5
<Route path=":userId" component={Profile} />
<Route
  path=":userId"
  render={routeProps => (
    <Profile {...routeProps} animate={true} />
  )}
/>

// v6
<Route path=":userId" element={<Profile />} />
<Route path=":userId" element={<Profile animate={true} />} />
```

而在 v6 版本中，`render` 与 `component` 方案合并成了 `element` 方案，可以轻松传递 props 且不需要透传 `roteProps` 参数。

### 更方便的嵌套路由

在 v5 版本中，嵌套路由需要通过 `useRouteMatch` 拿到 `match`，并通过 `match.path` 的拼接实现子路由：

```jsx
// v5
import {
  BrowserRouter,
  Switch,
  Route,
  Link,
  useRouteMatch
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/profile" component={Profile} />
      </Switch>
    </BrowserRouter>
  );
}

function Profile() {
  let match = useRouteMatch();

  return (
    <div>
      <nav>
        <Link to={`${match.url}/me`}>My Profile</Link>
      </nav>

      <Switch>
        <Route path={`${match.path}/me`}>
          <MyProfile />
        </Route>
        <Route path={`${match.path}/:id`}>
          <OthersProfile />
        </Route>
      </Switch>
    </div>
  );
}
```

在 v6 版本中省去了 `useRouteMatch` 这一步，支持直接用 `path` 表示相对路径：

```jsx
// v6
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router-dom";

// Approach #1
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="profile/*" element={<Profile />} />
      </Routes>
    </BrowserRouter>
  );
}

function Profile() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path="me" element={<MyProfile />} />
        <Route path=":id" element={<OthersProfile />} />
      </Routes>
    </div>
  );
}

// Approach #2
// You can also define all
// <Route> in a single place
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="profile" element={<Profile />}>
          <Route path=":id" element={<MyProfile />} />
          <Route path="me" element={<OthersProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Profile() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Outlet />
    </div>
  );
}
```

注意 `Outlet` 是渲染子路由的 Element。

### useNavigate 替代 useHistory

在 v5 版本中，主动跳转路由可以通过 `useHistory` 进行 `history.push` 等操作：

```jsx
// v5
import { useHistory } from "react-router-dom";

function MyButton() {
  let history = useHistory();
  function handleClick() {
    history.push("/home");
  }
  return <button onClick={handleClick}>Submit</button>;
}
```

而在 v6 版本中，可以通过 `useNavigate` 直接实现这个常用操作：

```jsx
// v6
import { useNavigate } from "react-router-dom";

function MyButton() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return <button onClick={handleClick}>Submit</button>;
}
```

react-router 内部对 history 进行了封装，如果需要 `history.replace`，可以通过 `{ replace: true }` 参数指定：

```jsx
// v5
history.push("/home");
history.replace("/home");

// v6
navigate("/home");
navigate("/home", { replace: true });
```

### 更小的体积 8kb

由于代码几乎重构，v6 版本的代码压缩后体积从 20kb 缩小到 8kb。

## 3 精读

react-router v6 源码中有一段比较核心的理念，笔者拿出来与大家分享，对一些框架开发是大有裨益的。我们看 `useRoutes` 这段代码节选：

```jsx
export function useRoutes(routes, basename = "", caseSensitive = false) {
  let {
    params: parentParams,
    pathname: parentPathname,
    route: parentRoute
  } = React.useContext(RouteContext);

  if (warnAboutMissingTrailingSplatAt) {
    // ...
  }

  basename = basename ? joinPaths([parentPathname, basename]) : parentPathname;

  let navigate = useNavigate();
  let location = useLocation();
  let matches = React.useMemo(
    () => matchRoutes(routes, location, basename, caseSensitive),
    [routes, location, basename, caseSensitive]
  );

  // ...

  // Otherwise render an element.
  let element = matches.reduceRight((outlet, { params, pathname, route }) => {
    return (
      <RouteContext.Provider
        children={route.element}
        value={{
          outlet,
          params: readOnly({ ...parentParams, ...params }),
          pathname: joinPaths([basename, pathname]),
          route
        }}
      />
    );
  }, null);

  return element;
}
```

可以看到，利用 `React.Context`，v6 版本在每个路由元素渲染时都包裹了一层 `RouteContext`。

拿更方便的路由嵌套来说：

> 在 v6 版本中省去了 `useRouteMatch` 这一步，支持直接用 `path` 表示相对路径。

这就是利用这个方案做到的，因为给每一层路由文件包裹了 Context，所以在每一层都可以拿到上一层的 `path`，因此在拼接路由时可以完全由框架内部实现，而不需要用户在调用时预先拼接好。

再以 `useNavigate` 举例，有人觉得 `navigate` 这个封装仅停留在形式层，但其实在功能上也有封装，比如如果传入但是一个相对路径，会根据当前路由进行切换，下面是 `useNavigate` 代码节选：

```jsx
export function useNavigate() {
  let { history, pending } = React.useContext(LocationContext);
  let { pathname } = React.useContext(RouteContext);

  let navigate = React.useCallback(
    (to, { replace, state } = {}) => {
      if (typeof to === "number") {
        history.go(to);
      } else {
        let relativeTo = resolveLocation(to, pathname);

        let method = !!replace || pending ? "replace" : "push";
        history[method](relativeTo, state);
      }
    },
    [history, pending, pathname]
  );

  return navigate;
}
```

可以看到，利用 `RouteContext` 拿到当前的 `pathname`，并根据 `resolveLocation` 对 `to` 与 `pathname` 进行路径拼接，而 `pathname` 就是通过 `RouteContext.Provider` 提供的。

### 巧用多层 Context Provider

很多时候我们利用 Context 停留在一个 `Provider`，多个 `useContext` 的层面上，这是 Context 最基础的用法，但相信读完 React Router v6 这篇文章，我们可以挖掘出 Context 更多的用法：多层 Context Provider。

**虽然说 Context Provider 存在多层会采取最近覆盖的原则，但这不仅仅是一条规避错误的功能，我们可以利用这个功能实现 React Router v6 这样的改良。**

为了更仔细说明这个特性，这里再举一个具体的例子：比如实现搭建渲染引擎时，每个组件都有一个 id，但这个 id 并不透出在组件的 props 上：

```jsx
const Input = () => {
  // Input 组件在画布中会自动生成一个 id，但这个 id 组件无法通过 props 拿到
};
```

此时如果我们允许 Input 组件内部再创建一个子元素，又希望这个子元素的 id 是由 Input 推导出来的，我们可能需要用户这么做：

```jsx
const Input = ({ id }) => {
  return <ComponentLoader id={id + "1"} />;
};
```

这样做有两个问题：

1. 将 id 暴露给 Input 组件，违背了之前设计的简洁性。
2. 组件需要对 id 进行拼装，很麻烦。

这里遇到的问题和 React Router 遇到的一样，我们可以将代码简化成下面这样，但功能不变吗？

```jsx
const Input = () => {
  return <ComponentLoader id="1" />;
};
```

答案是可以做到，我们可以利用 Context 实现这种方案。关键点就在于，渲染 Input 但组件容器需要包裹一个 Provider：

```jsx
const ComponentLoader = ({ id, element }) => {
  <Context.Provider value={{ id }}>{element}</Context.Provider>;
};
```

那么对于内部的组件来说，在不同层级下调用 `useContext` 拿到的 id 是不同的，这正是我们想要的效果：

```jsx
const ComponentLoader = ({id,element}) => {
  const { id: parentId } = useContext(Context)

  <Context.Provider value={{ id: parentId + id }}>
    {element}
  </Context.Provider>
}
```

这样我们在 `Input` 内部调用的 `<ComponentLoader id="1" />` 实际上拼接的实际 id 是 `01`，而这完全抛到了外部引擎层处理，用户无需手动拼接。

## 4 总结

React Router v6 完全利用 Hooks 重构后，不仅代码量精简了很多，还变得更好用了，等发正式版的时候可以快速升级一波。

另外从 React Router v6 做的这些优化中，我们从源码中挖掘到了关于 Context 更巧妙的用法，希望这个方法可以帮助你运用到其他更复杂的项目设计中。

> 讨论地址是：[精读《React Router v6》 · Issue #241 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/241)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
