每个不同的应用界面最好都有自己的 url 地址。在单页应用中,我们实际上总是在同一页上,即使有时处于不同的界面,路由地址仍然保持不变。

React 有 React Router 库,为管理 React 应用中的路由提供了一个解决方案。

教程:FullStackOpen2022 / Part 7 / React Router

本文以 routed-anecdotes 应用为例。对应练习 7.1-7.3。

注意: 本文中 React Router 版本为 5。版本 6 的 React Router 有一些突破性的改变,详见:React Router | Upgrading from v5

1. 使用 React Router

安装 React Router:

npm install react-router-dom

启用 React Router 提供的路由的应用:

App.js:

import React, { useState } from 'react'
import {
  Switch,
  Route,
  Link,
} from 'react-router-dom'

//...

const Menu = () => {
  return (
    <div>
      <Link to="/">anecdotes</Link>
      <Link to="/create">create new</Link>
      <Link to="/about">about</Link>
    </div>
  )
}

const App = () => {
  //...
  return (
    <div>
      //...
      <Switch>
        <Route path="/anecdotes/:id">
          <Anecdote anecdote={anecdote} />
        </Route>
        <Route path="/create">
          <CreateNew addNew={addNew} />
        </Route>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/">
          <AnecdoteList anecdotes={anecdotes} />
        </Route>
      </Switch>
      <Footer />
    </div>
  )
}

export default App

index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

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

路由,或者说在浏览器中基于 URL 的组件的有条件渲染,是通过将组件作为 BrowserRouter 组件的子组件,也就是在 <BrowserRouter> 标签中使用。

A <BrowserRouter> stores the current location in the browser’s address bar using clean URLs and navigates using the browser’s built-in history stack.

通常情况下,当地址栏中的 URL 发生变化时,浏览器会加载一个新页面。然而,在 HTML5 History API 的帮助下,BrowserRouter 能够在 React 应用中使用浏览器地址栏中的 URL 进行内部“路由”。因此,即使地址栏中的 URL 发生变化,页面的内容也只是使用 Javascript 进行操作,浏览器不会从服务器上加载新的内容。使用后退和前进的动作,以及做书签,仍然像在一个传统的网页上那样合乎逻辑。

在路由器 BrowserRouter 内部,用链接组件 <Link> 修改地址栏:

<Link to="/create">create new</Link>

基于浏览器 URL 渲染的组件由组件 <Route> 定义,并用一个 <Switch> 组件(或者 <Routes>)来包装要根据网址渲染的组件:

<Switch>
  <Route path="/anecdotes/:id">
    <Anecdote anecdote={anecdote} />
  </Route>
  <Route path="/create">
    <CreateNew addNew={addNew} />
  </Route>
  <Route path="/about">
    <About />
  </Route>
  <Route path="/">
    <AnecdoteList anecdotes={anecdotes} />
  </Route>
</Switch>

2. Parameterized Route

可以在 Route 中定义参数化的 URL:

<Route path="/anecdotes/:id">
  <Anecdotes anecdotes={anecdotes} />
</Route>

Anecdotes 组件可以通过 React Router 的 useParams 函数访问 URL 参数。

import {
  // ...
  useParams
} from "react-router-dom"

const Anecdotes = ({ anecdotes }) => {
  const id = useParams().id
  const anecdote = anecdotes.find(n => n.id === Number(id))
  //...
}

另一种方法是使用 React Router 的 useRouteMatch hook (或 useMatch)来计算参数。

import {
  //...
	useRouteMatch,
} from 'react-router-dom'

//...
const App = () => {
  //...
	const match = useRouteMatch('/anecdotes/:id')
  const anecdote = match
    ? anecdotes.find((anecdote) => anecdote.id === match.params.id)
    : null

  return (
    <div>
      //...
      <Switch>
        <Route path="/anecdotes/:id">
          <Anecdote anecdote={anecdote} />
        </Route>
        //...
      </Switch>
      <Footer />
    </div>
  )
}

3. 使用 Navigate 和 useNavigate 进行重定向

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

//...
<Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />

如果一个用户没有登录,Users 组件就不会被渲染。相反,用户会被组件 Navigate 重定向到登录视图。

React Router 的 useNavigate 函数则可以使浏览器的 URL 以编程方式改变。

import {
  // ...
  useNavigate
} from 'react-router-dom'

const Login = (props) => {
  const navigate = useNavigate()

  const onSubmit = (event) => {
    event.preventDefault()
    props.onLogin('username')
    navigate('/')
  }

  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          username: <input />
        </div>
        <div>
          password: <input type='password' />
        </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

随着用户的登录,调用 navigate("/"),使浏览器的 URL 改变为 /,应用渲染相应的组件Home。