精讀《React Router v6》

1 引言

React Router v6 alpha 版本發佈了,本週經過 A Sneak Peek at React Router v6 這篇文章分析一下帶來的改變。前端

2 概述

改名爲

一個不痛不癢的改動,使 API 命名更加規範。react

// 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 替代 Switchgit

// 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>
  );
}
複製代碼

升級

在 v5 版本立,想要給組件傳參數是不太直觀的,須要利用 RenderProps 的方式透傳 routePropsgithub

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 版本中,rendercomponent 方案合併成了 element 方案,能夠輕鬆傳遞 props 且不須要透傳 roteProps 參數。微信

更方便的嵌套路由

在 v5 版本中,嵌套路由須要經過 useRouteMatch 拿到 match,並經過 match.path 的拼接實現子路由:react-router

// 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 表示相對路徑:框架

// 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。dom

useNavigate 替代 useHistory

在 v5 版本中,主動跳轉路由能夠經過 useHistory 進行 history.push 等操做:ide

// 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 直接實現這個經常使用操做:優化

// 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 } 參數指定:

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

// v6
navigate("/home");
navigate("/home", { replace: true });
複製代碼

更小的體積 8kb

因爲代碼幾乎重構,v6 版本的代碼壓縮後體積從 20kb 縮小到 8kb。

3 精讀

react-router v6 源碼中有一段比較核心的理念,筆者拿出來與你們分享,對一些框架開發是大有裨益的。咱們看 useRoutes 這段代碼節選:

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 代碼節選:

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,並根據 resolveLocationtopathname 進行路徑拼接,而 pathname 就是經過 RouteContext.Provider 提供的。

巧用多層 Context Provider

不少時候咱們利用 Context 停留在一個 Provider,多個 useContext 的層面上,這是 Context 最基礎的用法,但相信讀完 React Router v6 這篇文章,咱們能夠挖掘出 Context 更多的用法:多層 Context Provider。

雖說 Context Provider 存在多層會採起最近覆蓋的原則,但這不單單是一條規避錯誤的功能,咱們能夠利用這個功能實現 React Router v6 這樣的改良。

爲了更仔細說明這個特性,這裏再舉一個具體的例子:好比實現搭建渲染引擎時,每一個組件都有一個 id,但這個 id 並不透出在組件的 props 上:

const Input = () => {
  // Input 組件在畫布中會自動生成一個 id,但這個 id 組件沒法經過 props 拿到
};
複製代碼

此時若是咱們容許 Input 組件內部再建立一個子元素,又但願這個子元素的 id 是由 Input 推導出來的,咱們可能須要用戶這麼作:

const Input = ({ id }) => {
  return <ComponentLoader id={id + "1"} />; }; 複製代碼

這樣作有兩個問題:

  1. 將 id 暴露給 Input 組件,違背了以前設計的簡潔性。
  2. 組件須要對 id 進行拼裝,很麻煩。

這裏遇到的問題和 React Router 遇到的同樣,咱們能夠將代碼簡化成下面這樣,但功能不變嗎?

const Input = () => {
  return <ComponentLoader id="1" />; }; 複製代碼

答案是能夠作到,咱們能夠利用 Context 實現這種方案。關鍵點就在於,渲染 Input 但組件容器須要包裹一個 Provider:

const ComponentLoader = ({ id, element }) => {
  <Context.Provider value={{ id }}>{element}</Context.Provider>;
};
複製代碼

那麼對於內部的組件來講,在不一樣層級下調用 useContext 拿到的 id 是不一樣的,這正是咱們想要的效果:

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

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索