(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
本篇內容以react-router v5為主,react-router在v6後有大幅度改變,可參考官方文件或是下方邦友回覆
https://reactrouter.com/en/main/upgrading/v5
<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中要求的參數移除或是設為非必須。
執行結果:
在前面的練習中,我們的Link在firstPage.js和SecondPage.js中都是固定的Layout,卻因為在不同的Component而導致要重新render。這並不是我們喜歡的狀況。所以,現在我們就讓背景和Link獨立出來成固定Layout,讓頁面改變的只有文字。
import React from 'react';
const Layout=(props)=>{
return(
);
}
export default Layout;
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>
);
}
return(
<h1 style={{color:"white",fontFamily:"Microsoft JhengHei"}}>我是第一頁</h1>
)
要放在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;
因為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中跟著被固定了,所以來讓它根據路徑來更改吧!
這邊要用到上一篇提過的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的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與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;
在我的範例中我其實還有幾個疑問,希望有大大可以解答:
謝謝~
如果要回傳404狀態碼請問有沒有更好的解法? 我上面是回傳一個頁面,但實際上statuscode仍然是200。
既然你的React程式可以渲染,那就代表對於user瀏覽器而言,你的前端程式碼是有正常從server被取得的。404的是你跟backend後續的溝通,像是ajax之類的
我的範例中Link透過state傳遞參數,那請問Route可以傳遞state嗎? 我嘗試過 <Route state={ test:"123"} ...>方式但useLocation接不到。
我沒有遇過這個case,不過這應該可以寫一個custom hook搭配useLocation
和useNavigate
實作到你要的效果?
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等狀態管理工具