本篇內容以react-router v5為主,react-router在v6後有大幅度改變,可參考官方文件或是下方邦友回覆
https://reactrouter.com/en/main/upgrading/v5

Link v.s <a>

如果我們想讓使用者用GUI導向不同頁面,過去會使用 <a> 。而react-router-dom提供了一個和 <a> 功能相同的元件 - Link。他的基礎語法是:

<Link to="路徑"> 顯示文字 </Link>

實際渲染時它會轉成<a>,並幫你根據前端路由導向正確href。

咦? 為什麼不用<a>就好,還要多生一個Link出來?

主要的原因是<a>的根路徑沒有辦法根據前端router去更動,而Link可以。以在我們的上一篇的練習為例,但如果要使用<a>的話,我們要自己幫<a>中的路徑加上「#」,才能導向正確路徑。

那我就加個「#」就好啦?

以前我也是這樣想,直到某天我必須「在伺服器的子使用者建立專案」。我的專案部署路徑是在主使用者domain/~子使用者名稱底下,預設也是讓使用者用這個路徑來存取我的網頁。可是<a>無法偵測子使用者。所以當我在自己電腦開發時使用<a href="/#/home"></a>,實際部署時卻會導向主使用者domain/#/home,而不是主使用者domain/~子使用者名稱#/home。導致最後我要把自己電腦開發和部署分成不同的版本,hen麻煩。

另外Link可以透過to以類似GET或POST的方法傳參數到前一篇講過的location物件中,有興趣的可以了解一下。

現在,我們來在firstPage.js和SecondPage.js中都加入可以切換頁面的UI。

先引入Link:

import {Link} from 'react-router-dom';

再加入可以導向兩個頁面的Link作為nav:

    return(
        <div style={StyleSheet}>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第一頁</h1>

記得先回去把route的path中要求的參數移除或是設為非必須。

執行結果:

在react-router-dom實現固定Layout的方法

在前面的練習中,我們的Link在firstPage.js和SecondPage.js中都是固定的Layout,卻因為在不同的Component而導致要重新render。這並不是我們喜歡的狀況。所以,現在我們就讓背景和Link獨立出來成固定Layout,讓頁面改變的只有文字。

step 1: 首先,請新增一個Layout.js,並宣告、輸出同名的函式。

import React from 'react';
const Layout=(props)=>{
    return(
export default Layout;

step 2: 接著,把剛剛背景跟Link的部分移過來

import {Link} from 'react-router-dom';
const Layout=(props)=>{
    const StyleSheet={
        width:"100vw",
        height:"100vh",
        backgroundColor:"#FF2E63",
        display: "flex",
        alignItems:"center",
        justifyContent:"center",
        flexDirection:"column"
    return(
        <div style={StyleSheet}>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>

step 3: 刪掉FirstPage.js和SecondPage.js中背景和Link的地方

    return(
            <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第一頁</h1>

step 4: 在App.js中,在Switch和Route之間,用Layout把Route夾起來

要放在Switch底下的原因是,Switch會把前面所提像是location這些傳給route元件的props傳給Layout,我們就能在Layout元件中根據不同路由參數呈現不同功能/樣貌。

import React from 'react';
import {HashRouter,Route,Switch} from "react-router-dom";
import FirstPage from "./FirstPage";
import SecondPage from "./SecondPage";
import Layout from "./Layout"; //記得要引入
const App=()=>{
    return( 
        <HashRouter>
            <Switch>
                <Layout>
                    <Route exact path="/" component={FirstPage}/>
                    <Route path="/second" component={SecondPage}/>
                </Layout>
            </Switch>
        </HashRouter>
export default App;

step 5: 回到Layout.js,在原本文字的地方加入props.children

因為route回傳的元素夾在Layout標籤內,所以要用children來取得

    return(
        <div style={StyleSheet}>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            {props.children}

以上是固定的背景和Link的做法,不過我們原本會一起改變的背景顏色也在Layout.js中跟著被固定了,所以來讓它根據路徑來更改吧!

Bonus: 讓Layout.js根據路徑改變背景顏色

這邊要用到上一篇提過的location當中的pathname,比較如果為/就讓backgroundColor變成紅色,否則變成青色。

    const StyleSheet={
        width:"100vw",
        height:"100vh",
        backgroundColor:(props.location.pathname==="/")?"#FF2E63":"#08D9D6",
        display: "flex",
        alignItems:"center",
        justifyContent:"center",
        flexDirection:"column"

這樣就完成了只更改不同route的Layout架構。

你可能會查到的舊資料

在過去react-router-dom實踐固定layout的方法是這樣的

    <Route path="/" component={Layout}/>
        <Route exact path="" component={FirstPage}/>
        <Route path="second" component={SecondPage}/>
    </Route>

但是大約在react-router-dom Ver.4 的時候,新增了route不能成為route的children的規定,所以必須使用前述的方式來實現固定layout。

綁定在Route中元件的props

如果是用Route的component去綁定元件的話,是沒有辦法綁props的。必須使用Route另一個props - render,以函式return值的方式綁定在它上面。以下是用這種方式「在SecondPage的props綁定一個為5的id」的語法:

<Route path="/second" render={()=>{return( <SecondPage id={5}/> )}}/>

原本綁component的方式是透過React.creactElement的方式創造元件。而這種綁render的方式等同於你在Route的props中製造並呼叫一個「渲染的元件的function」。在這一篇文章中有做詳細的解釋。

透過在Route中綁定元件的props,我們就能在Route與Route之間(子對子)、Route與Layout之間(子對子)、Route與做為Router控制中心的元件之間(子對父or父對子)做溝通。溝通的方法和我在【React.js入門 - 21】 Component的溝通所講的相同,就不再詳述了。

這篇是此系列最後一個獨立出來講的React.js工具,下一篇把之前都沒有特別講但很常用到的東西提一下(像是css檔之類的),然後我會開始慢慢把這個系列作收尾,來統整一下個人認為新手可能遇到的狀況。

看了這篇之後我自己實驗發生很多錯誤,最主要的問題在於現在react-router-dom v6 版本改了很多,如下改變:

  • 不再支援在route內使用客製化component,也就是說無法直接引用Layout。
  • 也不支援Switch,需要改用routes,並且改用"element"作為Component引入點。
  • 如果要在使用Link的時候傳遞參數,正常的props是無法使用的 (我理解是這樣,不太確定是否正確),要改用state傳遞,而被呼叫的Component要使用useLocation來接。
  • 我針對以上改變,以及松鼠大大文章最後提到的Route與Route之間(子對子)溝通概念,修改一下範例,目前測試可以work,但還請各位高手看看是否有更精簡的方式:

    index.js

    import MyApp3 from './functioncomponent/MyApp3';
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <MyApp3/>
      </React.StrictMode>
    

    MyApp3.js

    import { BrowserRouter,Route,Routes,Switch } from "react-router-dom";
    import FirstPage from './FirstPage';
    import SecondPage from './SecondPage';
    import NotFound from './NotFound';
    import MyLayout2 from './MyLayout2';
    import { useState } from "react";
    const MyApp3 = () => {
        const [value,setValue]=useState("111");
        const [value2,setValue2]=useState("112");
        return (
            <BrowserRouter>
                <Routes>    
                    <Route exact path="/" element={
                        <MyLayout2 >
                            <FirstPage value={value} clickHandle={(e)=>{setValue2(e.target.value)}}/>
                        </MyLayout2> 
                    <Route exact path="/second/:id?" element={
                        <MyLayout2>
                            <SecondPage value={value2} clickHandle={(e)=>{setValue(e.target.value)}}/>
                        </MyLayout2> 
                    <Route path="*" element={
                        <MyLayout2>
                            <NotFound />
                        </MyLayout2> 
                </Routes>
            </BrowserRouter>
    

    MyLayout2.js

    import React from 'react';
    import {Link, useLocation} from 'react-router-dom';
    const MyLayout2=(props)=>{
        const {pathname} = useLocation();
        const StyleSheet={
            width:"100vw",
            height:"100vh",
            backgroundColor:(pathname==="/")?"#FF2E63":"#08D9D6",
            display: "flex",
            alignItems:"center",
            justifyContent:"center",
            flexDirection:"column"
        return(
            <div style={StyleSheet}>
                    <Link to="/">點我連到第一頁</Link>
                    <Link to={{
                        pathname:'/second/helloworld',
                    }} state={{
                        title: 'foo2',
                    }} style={{marginLeft:"20px"}}>點我連到第二頁</Link>
                    <div></div>
                {props.children}
    export default MyLayout2;
    

    FirstPage.js

    import './FirstPage.css';
    const FirstPage=(props)=>{
        return (
                <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第一頁 {props.value}</h1>
                <button value={"456"} onClick={props.clickHandle}>button</button>
    export default FirstPage;
    

    SecondPage.js

    import React from 'react';
    import { useLocation, useParams } from 'react-router-dom';
    const SecondPage=(props)=>{
        const { id } = useParams();
        const { state } = useLocation();
        return(
                <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第二頁 {props.value}</h1>
                <button value={"789"} onClick={props.clickHandle}>button</button>
                    id: {id?id:""}
                    state: {state?state.title:""}
    export default SecondPage;
    

    NotFound.js

    const NotFound = () => {
        return (
                <div style={{
                        backgroundColor:"rgba(0,0,0,0.2)",
                        width:"100vw",
                        height:"100vh",
                        display: "flex",
                        alignItems: "center",
                        justifyContent:"center",}}>Nothing</div>
    export default NotFound;
    

    在我的範例中我其實還有幾個疑問,希望有大大可以解答:

  • 如果要回傳404狀態碼請問有沒有更好的解法? 我上面是回傳一個頁面,但實際上statuscode仍然是200。
  • 我的範例中Link透過state傳遞參數,那請問Route可以傳遞state嗎? 我嘗試過 <Route state={ test:"123"} ...>方式但useLocation接不到。
  • 目前範例中Route與Link好像都是使用類似get的方式傳遞參數,如果使用post的話要如何傳遞呢?
  • 如果要回傳404狀態碼請問有沒有更好的解法? 我上面是回傳一個頁面,但實際上statuscode仍然是200。

    既然你的React程式可以渲染,那就代表對於user瀏覽器而言,你的前端程式碼是有正常從server被取得的。404的是你跟backend後續的溝通,像是ajax之類的

    我的範例中Link透過state傳遞參數,那請問Route可以傳遞state嗎? 我嘗試過 <Route state={ test:"123"} ...>方式但useLocation接不到。

    我沒有遇過這個case,不過這應該可以寫一個custom hook搭配useLocationuseNavigate實作到你要的效果?

    https://reactrouter.com/en/main/hooks/use-navigate
    https://reactrouter.com/en/main/route/route#layout-routes

    目前範例中Route與Link好像都是使用類似get的方式傳遞參數,如果使用post的話要如何傳遞呢

    用form/Ajax跟server端溝通。如果你是純前端溝通,可以走JS solution就好,不需要多一個request。用一個最上層的state,或是用像context/redux等狀態管理工具