iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 28
0
Modern Web

給初入JS框架新手的React.js入門系列 第 28

【React.js入門 - 28】 我要更多更多的分頁 - react-router-dom (下)

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


本篇內容以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}>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            </nav> 
            <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第一頁</h1>
        </div>
    )

記得先回去把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}>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            </nav> 

        </div>
    );
}

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}>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/second" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            </nav> 
            {props.children}
        </div>
    );

以上是固定的背景和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.js入門 - 27】 我要更多更多的分頁 - react-router-dom (上)
下一篇
【React.js入門 - 29】 使用圖片、使用css檔、新手容易遇到的問題
系列文
給初入JS框架新手的React.js入門31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-01 11:05:01

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

  1. 不再支援在route內使用客製化component,也就是說無法直接引用Layout。
  2. 也不支援Switch,需要改用routes,並且改用"element"作為Component引入點。
  3. 如果要在使用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}>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to={{
                    pathname:'/second/helloworld',
                }} state={{
                    title: 'foo2',
                }} style={{marginLeft:"20px"}}>點我連到第二頁</Link>
                <div></div>
            </nav> 
            {props.children}
        </div>
    );
}
export default MyLayout2;

FirstPage.js

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

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(
        <div>
            <h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第二頁 {props.value}</h1>
            <button value={"789"} onClick={props.clickHandle}>button</button>
            <div>
                id: {id?id:""}
            </div>
            <div>
                state: {state?state.title:""}
            </div>
        </div>
    )
}

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;

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

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

謝謝~

Andy Chang iT邦研究生 4 級 ‧ 2023-02-04 16:28:59 檢舉

如果要回傳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等狀態管理工具

我要留言

立即登入留言