React-Router-Dom 4 入門


一、快速開始

返回目錄

首先要創建一個 Web App

#安裝官方腳手架 create-react-app
npm install -g create-react-app
#創建項目
create-react-app demo-app
#進入項目目錄
cd demo-app
#安裝 react-router-dom
npm install react-router-dom

示例1: 基礎路由

返回目錄

在這個例子裏,該路由匹配三個頁面(主頁、關於頁、用戶頁)。當你點擊不同的 <Link> 時,<Router> 會渲染與之匹配的 <Route>

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

export default function App() {
  return (<Router>
    <div>
      <nav>
        <ul>
          <li><Link to="/">主頁</Link></li>
          <li><Link to="/about">關於頁</Link></li>
          <li><Link to="/users">用戶頁</Link></li>
        </ul>
      </nav>
      
      {/* <Switch> 會查找與 URL 匹配的第一個 <Route> */}
      <Switch>
        <Route path="/about"><About /></Route>
        <Route path="/users"><Users /></Route>
        <Route path="/"><Home /></Route>
      </Switch>
    </div>
  </Router>);
}

function Home() {
  return <h2>主頁</h2>;
}

function About() {
  return <h2>關於頁</h2>;
}

function Users() {
  return <h2>用戶頁</h2>;
}

示例2: 嵌套路由

返回目錄

這個例子展示嵌套路由是怎麼工作的。/topics的路由會加載主題頁組件,並會渲染其頁面下的路由,並展示符合條件的路由的 topicId 值

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch,
  useParams
} from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li><Link to="/">主頁</Link></li>
          <li><Link to="/about">關於頁</Link></li>
          <li><Link to="/topics">主題頁</Link></li>
        </ul>

        <Switch>
          <Route path="/about"><About /></Route>
          <Route path="/topics"><Topics /></Route>
          <Route path="/"><Home /></Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>主頁</h2>;
}

function About() {
  return <h2>關於頁</h2>;
}

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

  return (
    <div>
      <h2>主題頁</h2>

      <ul>
        <li><Link to={`${match.url}/components`}>Components</Link></li>
        <li><Link to={`${match.url}/props-v-state`}>Props v. State</Link></li>
      </ul>
	  {/* 嵌套的<Switch>的路由 */}
      <Switch>
        <Route path={`${match.path}/:topicId`}><Topic /></Route>
        <Route path={match.path}><h3>Please select a topic.</h3></Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let { topicId } = useParams();
  return <h3>Requested topic ID: {topicId}</h3>;
}

二、重要的組件

返回目錄

在 React Router 裏有三種重要的組件

  • 路由器(Router), 包括 <BrowserRouter> 和 <HashRouter>
  • 路由(Route), 包括 <Route> 和 <Switch>
  • 導航(Navigation), 包括 <Link>, <NavLink>, 和 <Redirect>

路由器

返回目錄

每個React Router程序的核心應該是 路由器組件。對於Web項目,react-router-dom 提供 <BrowserRouter> 和 <HashRouter> 等路由器。兩者之間的主要區別是 存儲URL和 與Web服務器通信的方式

  • <BrowserRouter> 使用常規URL路徑。這些通常是外觀最好的URL,但是它們要求正確配置服務器。具體來說,您的Web服務器需要在所有由React Router客戶端管理的URL上提供相同的頁面。 Create React App在開發中即開即用地支持此功能,並附帶有關如何配置生產服務器的說明。
  • <HashRouter> 將當前位置存儲在URL的哈希部分中,因此URL看起來像http://example.com/#/your/page。由於哈希從不發送到服務器,因此這意味着不需要特殊的服務器配置。

要使用路由器,只需確保將其呈現在元素層次結構的根目錄下即可。通常,您會將頂級 <App> 元素包裝在路由器中,如下所示:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

function App() {
  return <h1>Hello React Router</h1>;
}

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

路由

返回目錄

有兩個路由匹配組件:Switch 和 Route。渲染 <Switch> 時,它將搜索其子元素 <Route> 元素,以找到其路徑與當前 URL 匹配的元素。當找到一個時,它將渲染該 <Route> 並忽略所有其他。這意味着您應將 <Route> 的路徑(通常較長)放置在的路徑之前。

如果沒有 <Route> 匹配,則 <Switch> 不呈現任何內容(null)。

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  Route
} from "react-router-dom";

function App() {
  return (
    <div>
      <Switch>
        {/* 如果當前的 URL 是 /about, 這個路由將被渲染,而其餘的被忽略 */}
        <Route path="/about"><About /></Route>

        {/* 請注意這兩個路由的順序。更具體的路徑 path="/contact/:id" 將放在
            path="/contact" 之前。因爲要查看個人的 contact 組件時,
            正確的路由將會渲染 */}
        <Route path="/contact/:id"><Contact /></Route>
        <Route path="/contact"><AllContacts /></Route>

        {/* 如果先前的路由都不渲染任何東西,這路由充當後備路由。

       		重要提示: path="/" 的路由將始終匹配,因爲所有的 URL 都是 / 開頭
            所以這就是我們爲什麼把它放在所有路由的最後 */}
        <Route path="/"><Home /></Route>
      </Switch>
    </div>
  );
}

ReactDOM.render(
  <Router><App /></Router>,
  document.getElementById("root")
);

需要注意一件重要的事是 <Route path> 匹配 URL 的開頭,而不是整個開頭。因此 <Route path=“/”> 將一直與 URL 匹配。因此,我們通常將此 <Route> 放置在 <Switch>裏的最後面。另一種可能的解決方案是使用 <Route exact path="/">,它與整個 URL 匹配。

注意:儘管 React Router 確實支持在 <Switch> 之外渲染 <Route> 元素,但是從 5.1 版本開始,我們建議您改用 useRouteMatch。此外,我們不建議您渲染一個沒有 path 的 <Route>,而是建議您使用 hook 來訪問所需的任何變量。

導航

返回目錄

React Router 提供 <Link> 組件來創建鏈接. 無論在哪使用 <Link>, 它將會變成一個 <a> 標籤在 html 上。

<Link to="/">Home</Link>
// <a href="/">Home</a>

<NavLink>是 <Link> 的一種特殊類型,當其屬性與當前位置匹配時,可以將其自身設置爲“active”。

<NavLink to="/react" activeClassName="hurray">React</NavLink>

// 當 URL 是 /react 時, 將會渲染:
// <a href="/react" className="hurray">React</a>

// 當 URL 是其它時, 將會渲染:
// <a href="/react">React</a>

任何時候要強制導航,可以使用 <Redirect>。渲染 <Redirect>時,它將使用其其屬性進行導航。

<Redirect to="/login" />

三、服務器端渲染

返回目錄

Rendering on the server is a bit different since it’s all stateless. The basic idea is that we wrap the app in a stateless <StaticRouter> instead of a <BrowserRouter>. We pass in the requested url from the server so the routes can match and a context prop we’ll discuss next.
由於服務器都是無狀態的,因此在服務器上的渲染有點不同。基本思想是,將應用程序包裝在無狀態 <StaticRouter> 中,而不是 <BrowserRouter> 中。我們從服務器傳入請求的 url,以便路由可以匹配,然後我們將討論 context 屬性。

// 客戶端
<BrowserRouter>
  <App/>
</BrowserRouter>

// 服務器端 (簡略)
<StaticRouter
  location={req.url}
  context={context}
>
  <App/>
</StaticRouter>

When you render a <Redirect> on the client, the browser history changes state and we get the new screen. In a static server environment we can’t change the app state. Instead, we use the context prop to find out what the result of rendering was. If we find a context.url, then we know the app redirected. This allows us to send a proper redirect from the server.
當您在客戶端上使用 <Redirect>時,瀏覽器歷史記錄將更改狀態,我們將獲得新屏幕。在靜態服務器環境中,我們無法更改應用程序狀態。相反,我們使用 context 屬性來找出渲染的結果。如果找到 context.url,則表明該應用已重定向。這使我們能夠從服務器發送適當的重定向。

const context = {};
const markup = ReactDOMServer.renderToString(
  <StaticRouter location={req.url} context={context}>
    <App />
  </StaticRouter>
);

if (context.url) {
  // Somewhere a `<Redirect>` was rendered
  redirect(301, context.url);
} else {
  // we're good, send the response
}

在應用程序中添加特殊的 context 信息

返回目錄

路由器只會添加 context.url。但是您可能希望將某些重定向重定向爲301,將其他重定向重定向爲302。或者,如果呈現了UI的某些特定分支,則可能要發送404響應,如果未授權,則要發送401。context 屬性是您的,因此您可以對其進行更改。這是區分 301 和 302 重定向的一種方法:

function RedirectWithStatus({ from, to, status }) {
  return (
    <Route
      render={({ staticContext }) => {
        // there is no `staticContext` on the client, so
        // we need to guard against that here
        if (staticContext) staticContext.status = status;
        return <Redirect from={from} to={to} />;
      }}
    />
  );
}

// 在你的app裏的任意位置
function App() {
  return (
    <Switch>
      {/* some other routes */}
      <RedirectWithStatus status={301} from="/users" to="/profiles" />
      <RedirectWithStatus
        status={302}
        from="/courses"
        to="/dashboard"
      />
    </Switch>
  );
}

// 在服務器上
const context = {};

const markup = ReactDOMServer.renderToString(
  <StaticRouter context={context}>
    <App />
  </StaticRouter>
);

if (context.url) {
  // 可以使用 `context.status` that
  // we added in RedirectWithStatus
  redirect(context.status, context.url);
}

404, 401, 或其他任何狀態

返回目錄

我們可以做與上述相同的事情。創建一個添加一些 context 的組件,並將其使用在應用程序中的任何位置以獲取不同的狀態代碼。

function Status({ code, children }) {
  return (
    <Route
      render={({ staticContext }) => {
        if (staticContext) staticContext.status = code;
        return children;
      }}
    />
  );
}

現在你可以把 在 staticContext 中添加代碼,在應用程序中的任何位置展示出來

function NotFound() {
  return (
    <Status code={404}>
      <div>
        <h1>對不起, 不能找到它.</h1>
      </div>
    </Status>
  );
}

function App() {
  return (
    <Switch>
      <Route path="/about" component={About} />
      <Route path="/dashboard" component={Dashboard} />
      <Route component={NotFound} />
    </Switch>
  );
}

全部放在一起

返回目錄

這不是一個真正的應用程序,但是它顯示了將它們組合在一起所需的所有常規內容。

服務器

import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom";

import App from "./App.js";

http
  .createServer((req, res) => {
    const context = {};

    const html = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );

    if (context.url) {
      res.writeHead(301, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `);
      res.end();
    }
  })
  .listen(3000);

客戶端

import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

import App from "./App.js";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("app")
);

數據載入

返回目錄

有許多種不同的方法,而且還沒有明確的最佳實踐,因此我們力求與任何一種方法融爲一體,而不規定或傾向於任何一種方法。我們相信路由器可以滿足您應用程序的限制。

主要限制是您要在渲染之前加載數據。 React Router 導出其內部使用的 matchPath 靜態函數以將位置與路由進行匹配。您可以在服務器上使用此功能來幫助確定呈現之前的數據依賴關係。

此方法的要旨依賴於靜態路由配置,該配置既可呈現您的路由,也可在呈現之前進行匹配以確定數據依賴性。

const routes = [
  {
    path: "/",
    component: Root,
    loadData: () => getSomeData()
  }
  // etc.
];

然後使用此配置在應用中呈現您的路由:

import { routes } from "./routes.js";

function App() {
  return (
    <Switch>
      {routes.map(route => (
        <Route {...route} />
      ))}
    </Switch>
  );
}

然後在服務器上,您將看到以下內容:

import { matchPath } from "react-router-dom";

// inside a request
const promises = [];
// use `some` to imitate `<Switch>` behavior of selecting only
// the first to match
routes.some(route => {
  // use `matchPath` here
  const match = matchPath(req.path, route);
  if (match) promises.push(route.loadData(match));
  return match;
});

Promise.all(promises).then(data => {
  // do something w/ the data so the client
  // can access it then render the app
});

最後,客戶將需要提取數據。同樣,我們不爲您的應用程序規定數據加載模式,但這是您需要實現的接觸點。

您可能對我們的React Router Config軟件包感興趣,以通過靜態路由配置幫助數據加載和服務器渲染。


四、代碼分割

返回目錄

One great feature of the web is that we don’t have to make our visitors download the entire app before they can use it. You can think of code splitting as incrementally downloading the app. To accomplish this we’ll use webpack, @babel/plugin-syntax-dynamic-import, and loadable-components.

webpack has built-in support for dynamic imports; however, if you are using Babel (e.g., to compile JSX to JavaScript) then you will need to use the @babel/plugin-syntax-dynamic-import plugin. This is a syntax-only plugin, meaning Babel won’t do any additional transformations. The plugin simply allows Babel to parse dynamic imports so webpack can bundle them as a code split. Your .babelrc should look something like this:

{
  "presets": ["@babel/preset-react"],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

loadable-components is a library for loading components with dynamic imports. It handles all sorts of edge cases automatically and makes code splitting simple! Here’s an example of how to use loadable-components:

import loadable from "@loadable/component";
import Loading from "./Loading.js";

const LoadableComponent = loadable(() => import("./Dashboard.js"), {
  fallback: <Loading />
});

export default class LoadableDashboard extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}

That’s all there is to it! Simply use LoadableDashboard (or whatever you named your component) and it will automatically be loaded and rendered when you use it in your application. The fallback is a placeholder component to show while the real component is loading.

這裏的所有都是它的!只需使用 LoadableDashboard(或任何您命名的組件),當您在應用程序中使用它時,它將自動加載並呈現。回退是一個佔位符組件,用於在加載實際組件時顯示。

完整的文檔點

代碼分割和服務端渲染

返回目錄

loadable-components includes a guide for server-side rendering.


五、Scroll Restoration

返回目錄

In earlier versions of React Router we provided out-of-the-box support for scroll restoration and people have been asking for it ever since. Hopefully this document helps you get what you need out of the scroll bar and routing!

Browsers are starting to handle scroll restoration with history.pushState on their own in the same manner they handle it with normal browser navigation. It already works in chrome and it’s really great. Here’s the Scroll Restoration Spec.

Because browsers are starting to handle the “default case” and apps have varying scrolling needs (like this website!), we don’t ship with default scroll management. This guide should help you implement whatever scrolling needs you have.

Scroll to top

返回目錄

Most of the time all you need is to “scroll to the top” because you have a long content page, that when navigated to, stays scrolled down. This is straightforward to handle with a <ScrollToTop> component that will scroll the window up on every navigation:

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

export default function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

If you aren’t running React 16.8 yet, you can do the same thing with a React.Component subclass:

import React from "react";
import { withRouter } from "react-router-dom";

class ScrollToTop extends React.Component {
  componentDidUpdate(prevProps) {
    if (
      this.props.location.pathname !== prevProps.location.pathname
    ) {
      window.scrollTo(0, 0);
    }
  }

  render() {
    return null;
  }
}

export default withRouter(ScrollToTop);

Then render it at the top of your app, but below Router

function App() {
  return (
    <Router>
      <ScrollToTop />
      <App />
    </Router>
  );
}

If you have a tab interface connected to the router, then you probably don’t want to be scrolling to the top when they switch tabs. Instead, how about a <ScrollToTopOnMount> in the specific places you need it?

import { useEffect } from "react";

function ScrollToTopOnMount() {
  useEffect(() => {
    window.scrollTo(0, 0);
  }, []);

  return null;
}

// Render this somewhere using:
// <Route path="..." children={<LongContent />} />
function LongContent() {
  return (
    <div>
      <ScrollToTopOnMount />

      <h1>Here is my long content page</h1>
      <p>...</p>
    </div>
  );
}

Again, if you aren’t running React 16.8 yet, you can do the same thing with a React.Component subclass:

import React from "react";

class ScrollToTopOnMount extends React.Component {
  componentDidMount() {
    window.scrollTo(0, 0);
  }

  render() {
    return null;
  }
}

// Render this somewhere using:
// <Route path="..." children={<LongContent />} />
class LongContent extends React.Component {
  render() {
    return (
      <div>
        <ScrollToTopOnMount />

        <h1>Here is my long content page</h1>
        <p>...</p>
      </div>
    );
  }
}

Generic Solution

返回目錄

For a generic solution (and what browsers are starting to implement natively) we’re talking about two things:

  1. Scrolling up on navigation so you don’t start a new screen scrolled to the bottom
  2. Restoring scroll positions of the window and overflow elements on “back” and “forward” clicks (but not Link clicks!)

At one point we were wanting to ship a generic API. Here’s what we were headed toward:

<Router>
  <ScrollRestoration>
    <div>
      <h1>App</h1>

      <RestoredScroll id="bunny">
        <div style={{ height: "200px", overflow: "auto" }}>
          I will overflow
        </div>
      </RestoredScroll>
    </div>
  </ScrollRestoration>
</Router>

First, ScrollRestoration would scroll the window up on navigation. Second, it would use location.key to save the window scroll position and the scroll positions of RestoredScroll components to sessionStorage. Then, when ScrollRestoration or RestoredScroll components mount, they could look up their position from sessionsStorage.

The tricky part was defining an “opt-out” API for when you don’t want the window scroll to be managed. For example, if you have some tab navigation floating inside the content of your page you probably don’t want to scroll to the top (the tabs might be scrolled out of view!).

When we learned that Chrome manages scroll position for us now, and realized that different apps are going to have different scrolling needs, we kind of lost the belief that we needed to provide something–especially when people just want to scroll to the top (which you saw is straight-forward to add to your app on your own).

Based on this, we no longer feel strongly enough to do the work ourselves (like you we have limited time!). But, we’d love to help anybody who feels inclined to implement a generic solution. A solid solution could even live in the project. Hit us up if you get started on it 😃


六、Philosophy

返回目錄

This guide’s purpose is to explain the mental model to have when using React Router. We call it “Dynamic Routing”, which is quite different from the “Static Routing” you’re probably more familiar with.

Static Routing

返回目錄

If you’ve used Rails, Express, Ember, Angular etc. you’ve used static routing. In these frameworks, you declare your routes as part of your app’s initialization before any rendering takes place. React Router pre-v4 was also static (mostly). Let’s take a look at how to configure routes in express:

// Express Style routing:
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();

Note how the routes are declared before the app listens. The client side routers we’ve used are similar. In Angular you declare your routes up front and then import them to the top-level AppModule before rendering:

// Angular Style routing:
const appRoutes: Routes = [
  {
    path: "crisis-center",
    component: CrisisListComponent
  },
  {
    path: "hero/:id",
    component: HeroDetailComponent
  },
  {
    path: "heroes",
    component: HeroListComponent,
    data: { title: "Heroes List" }
  },
  {
    path: "",
    redirectTo: "/heroes",
    pathMatch: "full"
  },
  {
    path: "**",
    component: PageNotFoundComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(appRoutes)]
})
export class AppModule {}

Ember has a conventional routes.js file that the build reads and imports into the application for you. Again, this happens before your app renders.

// Ember Style Router:
Router.map(function() {
  this.route("about");
  this.route("contact");
  this.route("rentals", function() {
    this.route("show", { path: "/:rental_id" });
  });
});

export default Router;

Though the APIs are different, they all share the model of “static routes”. React Router also followed that lead up until v4.

To be successful with React Router, you need to forget all that! 😮

Backstory

返回目錄

To be candid, we were pretty frustrated with the direction we’d taken React Router by v2. We (Michael and Ryan) felt limited by the API, recognized we were reimplementing parts of React (lifecycles, and more), and it just didn’t match the mental model React has given us for composing UI.

We were walking through the hallway of a hotel just before a workshop discussing what to do about it. We asked each other: “What would it look like if we built the router using the patterns we teach in our workshops?”

It was only a matter of hours into development that we had a proof-of-concept that we knew was the future we wanted for routing. We ended up with API that wasn’t “outside” of React, an API that composed, or naturally fell into place, with the rest of React. We think you’ll love it.

Dynamic Routing

返回目錄

When we say dynamic routing, we mean routing that takes place as your app is rendering, not in a configuration or convention outside of a running app. That means almost everything is a component in React Router. Here’s a 60 second review of the API to see how it works:

First, grab yourself a Router component for the environment you’re targeting and render it at the top of your app.

// react-native
import { NativeRouter } from "react-router-native";

// react-dom (what we'll use here)
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  el
);

Next, grab the link component to link to a new location:

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
  </div>
);

Finally, render a Route to show some UI when the user visits /dashboard.

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
    <div>
      <Route path="/dashboard" component={Dashboard} />
    </div>
  </div>
);

The Route will render <Dashboard {…props}/> where props are some router specific things that look like { match, location, history }. If the user is not at /dashboard then the Route will render null. That’s pretty much all there is to it.

Nested Routes

返回目錄

Lots of routers have some concept of “nested routes”. If you’ve used versions of React Router previous to v4, you’ll know it did too! When you move from a static route configuration to dynamic, rendered routes, how do you “nest routes”? Well, how do you nest a div?

const App = () => (
  <BrowserRouter>
    {/* here's a div */}
    <div>
      {/* here's a Route */}
      <Route path="/tacos" component={Tacos} />
    </div>
  </BrowserRouter>
);

// when the url matches `/tacos` this component renders
const Tacos = ({ match }) => (
  // here's a nested div
  <div>
    {/* here's a nested Route,
        match.url helps us make a relative path */}
    <Route path={match.url + "/carnitas"} component={Carnitas} />
  </div>
);

See how the router has no “nesting” API? Route is just a component, just like div. So to nest a Route or a div, you just … do it.

Let’s get trickier.

Responsive Routes

返回目錄

Consider a user navigates to /invoices. Your app is adaptive to different screen sizes, they have a narrow viewport, and so you only show them the list of invoices and a link to the invoice dashboard. They can navigate deeper from there.

Small Screen
url: /invoices

+----------------------+
|                      |
|      Dashboard       |
|                      |
+----------------------+
|                      |
|      Invoice 01      |
|                      |
+----------------------+
|                      |
|      Invoice 02      |
|                      |
+----------------------+
|                      |
|      Invoice 03      |
|                      |
+----------------------+
|                      |
|      Invoice 04      |
|                      |
+----------------------+

On a larger screen we’d like to show a master-detail view where the navigation is on the left and the dashboard or specific invoices show up on the right.

Large Screen
url: /invoices/dashboard

+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |   Unpaid:             5   |
+----------------------+                           |
|                      |   Balance:   $53,543.00   |
|      Invoice 01      |                           |
|                      |   Past Due:           2   |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |                           |
|                      |   +-------------------+   |
+----------------------+   |                   |   |
|                      |   |  +    +     +     |   |
|      Invoice 03      |   |  | +  |     |     |   |
|                      |   |  | |  |  +  |  +  |   |
+----------------------+   |  | |  |  |  |  |  |   |
|                      |   +--+-+--+--+--+--+--+   |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

Now pause for a minute and think about the /invoices url for both screen sizes. Is it even a valid route for a large screen? What should we put on the right side?

Large Screen
url: /invoices
+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 01      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |             ???           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 03      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

On a large screen, /invoices isn’t a valid route, but on a small screen it is! To make things more interesting, consider somebody with a giant phone. They could be looking at /invoices in portrait orientation and then rotate their phone to landscape. Suddenly, we have enough room to show the master-detail UI, so you ought to redirect right then!

React Router’s previous versions’ static routes didn’t really have a composable answer for this. When routing is dynamic, however, you can declaratively compose this functionality. If you start thinking about routing as UI, not as static configuration, your intuition will lead you to the following code:

const App = () => (
  <AppLayout>
    <Route path="/invoices" component={Invoices} />
  </AppLayout>
);

const Invoices = () => (
  <Layout>
    {/* always show the nav */}
    <InvoicesNav />

    <Media query={PRETTY_SMALL}>
      {screenIsSmall =>
        screenIsSmall ? (
          // small screen has no redirect
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
          </Switch>
        ) : (
          // large screen does!
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
            <Redirect from="/invoices" to="/invoices/dashboard" />
          </Switch>
        )
      }
    </Media>
  </Layout>
);

As the user rotates their phone from portrait to landscape, this code will automatically redirect them to the dashboard. The set of valid routes change depending on the dynamic nature of a mobile device in a user’s hands.

This is just one example. There are many others we could discuss but we’ll sum it up with this advice: To get your intuition in line with React Router’s, think about components, not static routes. Think about how to solve the problem with React’s declarative composability because nearly every “React Router question” is probably a “React question”.

七、Testing

返回目錄

React Router relies on React context to work. This affects how you can test your components that use our components.

Context

返回目錄

If you try to unit test one of your components that renders a <Link> or a <Route>, etc. you’ll get some errors and warnings about context. While you may be tempted to stub out the router context yourself, we recommend you wrap your unit test in one of the Router components: the base Router with a history prop, or a <StaticRouter>, <MemoryRouter>, or <BrowserRouter> (if window.history is available as a global in the test enviroment).

Using MemoryRouter or a custom history is recommended in order to be able to reset the router between tests.

class Sidebar extends Component {
  // ...
  render() {
    return (
      <div>
        <button onClick={this.toggleExpand}>expand</button>
        <ul>
          {users.map(user => (
            <li>
              <Link to={user.path}>{user.name}</Link>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

// broken
test("it expands when the button is clicked", () => {
  render(<Sidebar />);
  click(theButton);
  expect(theThingToBeOpen);
});

// fixed!
test("it expands when the button is clicked", () => {
  render(
    <MemoryRouter>
      <Sidebar />
    </MemoryRouter>
  );
  click(theButton);
  expect(theThingToBeOpen);
});

Starting at specific routes

返回目錄

<MemoryRouter> supports the initialEntries and initialIndex props, so you can boot up an app (or any smaller part of an app) at a specific location.

test("current user is active in sidebar", () => {
  render(
    <MemoryRouter initialEntries={["/users/2"]}>
      <Sidebar />
    </MemoryRouter>
  );
  expectUserToBeActive(2);
});

Navigating

返回目錄

We have a lot of tests that the routes work when the location changes, so you probably don’t need to test this stuff. But if you need to test navigation within your app, you can do so like this:

// app.js (a component file)
import React from "react";
import { Route, Link } from "react-router-dom";

// our Subject, the App, but you can test any sub
// section of your app too
const App = () => (
  <div>
    <Route
      exact
      path="/"
      render={() => (
        <div>
          <h1>Welcome</h1>
        </div>
      )}
    />
    <Route
      path="/dashboard"
      render={() => (
        <div>
          <h1>Dashboard</h1>
          <Link to="/" id="click-me">
            Home
          </Link>
        </div>
      )}
    />
  </div>
);
// you can also use a renderer like "@testing-library/react" or "enzyme/mount" here
import { render, unmountComponentAtNode } from "react-dom";
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from "react-router-dom";

// app.test.js
it("navigates home when you click the logo", async => {
  // in a real test a renderer like "@testing-library/react"
  // would take care of setting up the DOM elements
  const root = document.createElement('div');
  document.body.appendChild(root);

  // Render app
  render(
    <MemoryRouter initialEntries={['/my/initial/route']}>
      <App />
    <MemoryRouter>,
    root
  );

  // Interact with page
  act(() => {
    // Find the link (perhaps using the text content)
    const goHomeLink = document.querySelector('#nav-logo-home');
    // Click it
    goHomeLink.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  // Check correct page content showed up
  expect(document.body.textContent).toBe('Home');
});

Checking location in tests

返回目錄

You shouldn’t have to access the location or history objects very often in tests, but if you do (such as to validate that new query params are set in the url bar), you can add a route that updates a variable in the test:

// app.test.js
test("clicking filter links updates product query params", () => {
  let history, location;
  render(
    <MemoryRouter initialEntries={["/my/initial/route"]}>
      <App />
      <Route
        path="*"
        render={({ history, location }) => {
          history = history;
          location = location;
          return null;
        }}
      />
    </MemoryRouter>,
    node
  );

  act(() => {
    // example: click a <Link> to /products?id=1234
  });

  // assert about url
  expect(location.pathname).toBe("/products");
  const searchParams = new URLSearchParams(location.search);
  expect(searchParams.has("id")).toBe(true);
  expect(searchParams.get("id")).toEqual("1234");
});

Alternatives

  1. You can also use BrowserRouter if your test environment has the browser globals window.location and window.history (which is the default in Jest through JSDOM, but you cannot reset the history between tests).
  2. Instead of passing a custom route to MemoryRouter, you can use the base Router with a history prop from the history package:
    // app.test.js
    import { createMemoryHistory } from "history";
    import { Router } from "react-router";
    
    test("redirects to login page", () => {
      const history = createMemoryHistory();
      render(
        <Router history={history}>
          <App signedInUser={null} />
        </Router>,
        node
      );
      expect(history.location.pathname).toBe("/login");
    });
    

React Testing Library

返回目錄

See an example in the official documentation: Testing React Router with React Testing Library


八、集成 Redux

返回目錄

Redux 是 React 生態系統的重要組成部分。我們希望使 React Router 和 Redux 的集成儘可能無縫,以供希望同時使用這兩者的人使用。

阻止更新

返回目錄

一般來說,React Router 和 Redux 一起工作得很好。
不過,有時應用程序可能會有一個組件,在位置更改時不會更新(子路由 或 active 的 nav link 不會更新)。

如果發生以下情況:

  1. 這個組件使用了 connect()(Comp).
  2. 這個組件不是路由組件,意思是它不是這樣渲染的: <Route component={SomeConnectedThing}/>

The problem is that Redux implements shouldComponentUpdate and there’s no indication that anything has changed if it isn’t receiving props from the router. This is straightforward to fix. Find where you connect your component and wrap it in withRouter.
問題是 Redux 實現了 shouldComponentUpdate,如果它沒有從 Router 那接收 prop,就沒有任何跡象表明有任何變化。這很容易解決。找到 connect 組件的位置並用 withRouter 將其包起來。

// 之前
export default connect(mapStateToProps)(Something)

// 之後
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))

深度集成

返回目錄

有些人想:

  1. 將路由數據與 store 同步,並從 store 中進行訪問。
  2. 能夠通過 dispatching actions 進行導航。
  3. 支持 Redux devtools 中路由更改的時差調試

所有這些都需要更深入的整合。

我們建議不要將您的路由保留在您的Redux store。原因:

  1. 路由數據已經是大多數關心它的組件的 prop。無論是來自 store 還是路由器,組件的代碼基本上是相同的。
  2. 在大多數情況下,可以使用 Link、NavLink 和 Redirect 來進行導航操作。有時,在 action 啓動的異步任務之後,可能還需要用編程的方式進行導航。例如,您可以在用戶提交登錄表單時dispatch 一個 action。 然後 thunk, saga 或其他異步處理程序驗證憑據,如果成功,則需要以某種方式導航到新頁面。這裏的解決方案只是在 action 的 payload 中包含 history 對象(提供給所有路由組件),並且您的異步處理程序可以在適當的時候使用它來導航。
  3. Route changes are unlikely to matter for time travel debugging. The only obvious case is to debug issues with your router/store synchronization, and this problem goes away if you don’t synchronize them at all.
    對於時差調試來說,路由更改不太重要。唯一明顯的情況是調試路由器/store 同步的問題,如果根本不同步,這個問題就會消失。

但是,如果您強烈希望將路由與 store 同步,則可能需要嘗試連接的React Router,這是React Router v4 和 Redux 的第三方綁定。

九、靜態路由

返回目錄

React Router 之前的版本使用靜態路由來配置應用程序的路由。這樣可以在渲染之前檢查和匹配路由。由於 v4 轉移到動態組件而不是路由配置,因此以前的一些用例變得不那麼好理解和複雜。

We are developing a package to work with static route configs and React Router, to continue to meet those use-cases. It is under development now but we’d love for you to try it out and help out.
我們正在開發一個可與靜態路由配置和 React Router 配合使用的 package,以繼續滿足這些用例。現在正在開發中,但我們希望您能嘗試一下並提供幫助。

React Router Config

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章