感覺這篇文特別長。
簡述
雖然以前有學過 React router dom,也有實際用過的經驗,但感覺自己對有些東西還是不夠熟,所以今天就來重溫這個主題。
Router、Routes、Route,你們到底差在哪?
先讓我們看一段 code 再來解釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from 'react'; import Home from "./Home"; import About "./About"; import Profile from "./Profile"; import { HashRouter as Router, Routes, Route } from "react-router-dom"
export const App: React.FC = () => { return ( <Router> <a href='/home'>Show in every page</a> <Routes> <Route path="/" element={<Home />}></Route> <Route path="/about" element={<About />}></Route> <Route path="/profile" element={<Profile />}></Route> </Routes> </Router> ) }
export default App;
|
首先 Router
其實是 BrowserRouter
的 HashRouter
的別名,因為原名太長了才會改用這個名字來代表。
至於 BrowserRouter
的 HashRouter
的最主要的差異在於網址會不會有 #
,知道這樣就夠了。
至於 Routers
跟 Route
的差別顯而易見,Routers
是用來把所有路由給包住的(注意字尾的 s),而 Route
則是用來顯示哪個網址要顯示哪個 Component。
另外這個例子裡還特意放了一個 <a>
在 Routes
外面,其實是想順便說一件事情,就是像導覽列這種每一個頁面都會用到的 Component,我們就不會把它放在 Routes
裡。
基本結構與 404 頁面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import About from './pages/About' import Home from './pages/Home' import Profile from './pages/Profile'
export const App: React.FC = () => { return ( <Router> <Routes> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> <Route path='/profile' element={<Profile />} /> </Routes> </Router> ) }
export default App
|
就跟一開始的範例一樣,我們會在 Route
透過 path
和 element
來指定「哪個路由顯示哪個 Component」。
但如果我們想要有個 404 頁面怎麼辦?可以這樣做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import About from './pages/About' import ErrorPage from './pages/ErrorPage' import Home from './pages/Home' import Profile from './pages/Profile'
export const App: React.FC = () => { return ( <Router> <Routes> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> <Route path='/profile' element={<Profile />} /> <Route path='*' element={<ErrorPage />} /> </Routes> </Router> ) }
export default App
|
這邊的技巧是把 path
指定為 *
並且放在最下面(一定要這樣子)。
為什麼要這樣做?因為只有當上面都匹配不到時才會套用最後一個,所以我們只要把它對應到 404 的 Component 就好囉。
切換路由
切換路由主要有兩種方式,一種是透過 Link
(react-router-dom 提供的元件),另一種是透過 useNavigate
。
這邊先來看 Link
的做法,首先要先建立一個 Nav
元件來當作導覽列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from 'react' import { Link } from 'react-router-dom'
const Nav: React.FC = () => { return ( <ul> <li> <Link to='/'>Home</Link> </li> <li> <Link to='/about'>About</Link> </li> <li> <Link to='/profile'>Profile</Link> </li> </ul> ) }
export default Nav
|
<Link>
的本體就是 <a>
而已,所以你也可以用 <a>
來取代。但通常會建議用 <Link>
,因為如果用的是 HashRouter
就不能像上面一樣只寫 /about
,而是得自己加上 #
。
接著再把 Nav
放到主頁面中就行囉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import About from './pages/About' import ErrorPage from './pages/ErrorPage' import Home from './pages/Home' import Profile from './pages/Profile' import Nav from './pages/Nav'
export const App: React.FC = () => { return ( <Router> <Nav /> <Routes> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> <Route path='/profile' element={<Profile />} /> <Route path='*' element={<ErrorPage />} /> </Routes> </Router> ) }
|
接著是第二種做法,用 useNavigate
。這個可能會用在填完表單時要跳轉時的情境,換句話說就是透過 JS 來做切換:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from 'react' import { useNavigate } from 'react-router-dom'
const About: React.FC = () => { const navigate = useNavigate()
return ( <div> About page <button onClick={() => navigate('/')}>Go back to home page</button> </div> ) }
export default About
|
動態路由
舉一個常見的例子,如果你想造訪某個人的 profile,那網址一定是長成這樣:
https://aaa.com/profile/peanu
https://aaa.com/profile/ppb
https://aaa.com/profile/huli
結構都是以 /profile/名稱
來組成,可是因為名稱
不是固定的值,所以才會說這是「動態」的。
實現的方式也不難,直接來看 code。
首先先調整一下路由的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import About from './pages/About' import ErrorPage from './pages/ErrorPage' import Home from './pages/Home' import Profile from './pages/Profile' import Nav from './pages/Nav'
export const App: React.FC = () => { return ( <Router> <Nav /> <Routes> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> {/* 後面接 /:params */} <Route path='/profile/:username' element={<Profile />} /> <Route path='*' element={<ErrorPage />} /> </Routes> </Router> ) }
export default App
|
只要加上 :
就代表這是動態的值,並且會用一個變數的方式來代表這個值。
接著到 <Profile />
的部分做點調整,這邊會用到 useParams
這個 hook。
它的用途就是讓我們取得動態路由的內容,簡單來說,如果網址是 /profile/peanu
,回傳值就會是 { username: "peanu" }
,以此類推。
1 2 3 4 5 6 7 8 9 10
| import React from 'react' import { useParams } from 'react-router-dom'
const Profile: React.FC = () => { const { username } = useParams()
return <div>Hi, This is {username}'s Profile page</div> }
export default Profile
|
調整完以後,現在到 /profile/paenu
時結果就會是 Hi, This is peanu's Profile page
,其他也是以此類推。
加入動態路由的功能後,就可以根據路由的值來決定要呈現什麼內容,是非常實用的功能。
好,到這邊先中場休息。等一下會介紹我覺得比較複雜的東西:巢狀路由
另外有需要原始碼的話可以參考這裡。
巢狀路由
這邊會用底下的範例來解說:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import User from './pages/User' import Nav from './pages/Nav' import Home from './pages/Home' import NoMatch from './pages/Nomatch'
export const App: React.FC = () => { return ( <Router> <Nav /> <Routes> <Route index element={<Home />} /> <Route path='home' element={<Home />} /> <Route path='user' element={<User />} /> <Route path='*' element={<NoMatch />} /> </Routes> </Router> ) }
export default App
|
我們希望 /user
底下有 /user/profile
跟 /user/account
這兩個頁面,像這樣:
首先先到 <User />
中建立用來跳轉的連結:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from 'react' import { Link } from 'react-router-dom'
const User: React.FC = () => { return ( <div className='container'> <h2>User: PeaNu</h2> <ul> <li> <Link to='profile'>Profile</Link> </li> <li> <Link to='account'>Account</Link> </li> </ul> </div> ) }
export default User
|
路徑的部分可以寫成相對路徑或絕對路徑:
/user/profile
(絕對)
profile
(相對)
但請別寫成 /profile
,因為用 /
來開頭就會被當成絕對路徑,所以會跳到 https://aaa.com/profile
這個位置。
做到這邊以後,點了連接應該會跳到 404 頁面,這是因為我們還沒有去設定路由,讓 React 知道 /user/profile
要顯示哪個 Component,所以接著來對這部分做設定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import User from './pages/User' import Nav from './pages/Nav' import Home from './pages/Home' import NoMatch from './pages/Nomatch' import Profile from './pages/Profile' import Account from './pages/Account'
export const App: React.FC = () => { return ( <Router> <Nav /> <Routes> <Route index element={<Home />} /> <Route path='home' element={<Home />} /> <Route path='user' element={<User />}> {/* 因為是巢狀,所以得包在 user 的 Route 裡面 */} <Route path='profile' element={<Profile />}></Route> <Route path='account' element={<Account />}></Route> </Route> <Route path='*' element={<NoMatch />} /> </Routes> </Router> ) }
export default App
|
附註:關於 <Profile />
和 <Account />
這兩個元件就麻煩你自己去建立一下,這邊就不貼出來佔版面了。
做到這以後看起來好像就差不多了?但其實還沒有,目前點連結後會發現網址確實會變成 /user/profile
,可是並不會顯示 <Profile />
的內容。
這是因為我們漏掉了 Outlet
這個東西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react' import { Link, Outlet } from 'react-router-dom'
const User: React.FC = () => { return ( <div className='container'> <h2>User: PeaNu</h2> <ul> <li> <Link to='profile'>Profile</Link> </li> <li> <Link to='account'>Account</Link> </li> </ul> {/* 加上 Outlet */} <Outlet /> </div> ) }
export default User
|
Outlet
是負責把匹配的子路由給顯示出來的 Component,這邊因為 Outlet
是放在 User
中,所以會根據剛剛寫的路由來匹配 profile
和 account
這兩個路徑。
順道一提,如果你想避免 /user/profile/xxxx
的時候跳轉到 404,你可以透過 *
來處理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <router> <nav /> <routes> <route index element={<Home />} /> <route path='home' element={<Home />} /> <route path='user' element={<User />}> {/* 加上 * 號 */} <route path='profile/*' element={<Profile />}></route> <route path='account/*' element={<Account />}></route> </route> <route path='*' element={<Nomatch />} /> </routes> <router>
|
OK,以上就是巢狀路由的教學,如果想參考原始碼的話可以到這邊來看。
另一種巢狀結構的思維
剛剛介紹的巢狀結構,實際在使用時看起來是像這樣:
可是如果我希望點下 profile 時,是跳轉到一個全新的頁面,而不是像上面那樣保留導覽列的情境呢?像這樣:
這時候就要用比較 tricky 的作法了(也有人說是寫 App 的思維)。
首先要先回憶一下 Outlet
這個 Component 的用途,它的用途是把「對應的子路由顯示出來」,而剛剛我們是把它寫在 <User />
裡面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react' import { Link, Outlet } from 'react-router-dom'
const User: React.FC = () => { return ( <div className='container'> <h2>User: PeaNu</h2> <ul> <li> <Link to='profile'>Profile</Link> </li> <li> <Link to='account'>Account</Link> </li> </ul> {/* 寫在這裡 */} <Outlet /> </div> ) }
export default User
|
所以當我們在造訪 /user/profile
的時候就會把對應的 Component 顯示到 <Outlet />
裡面。也就是說不管我們怎麼切換路由,導覽列的內容會永遠擺在 <Outlte />
上面。
既然如此,我們就不能把 <Outlet />
放在 <User />
中,可是這樣的話要放在哪裡?
其實答案可能沒有你想像中的複雜,先來看 code 吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import React from 'react' import { HashRouter as Router, Routes, Route, Outlet } from 'react-router-dom' import User from './pages/User' import Nav from './pages/Nav' import Home from './pages/Home' import NoMatch from './pages/Nomatch' import Profile from './pages/Profile' import Account from './pages/Account'
export const App: React.FC = () => { return ( <Router> <Nav /> <Routes> <Route index element={<Home />} /> <Route path='home' element={<Home />} /> {/* 放在這裡 */} <Route path='user' element={<Outlet />}> {/* 這個的意思就是 /user 時顯示 <User /> */} <Route path='' element={<User />} /> <Route path='profile/*' element={<Profile />}></Route> <Route path='account/*' element={<Account />}></Route> </Route> <Route path='*' element={<NoMatch />} /> </Routes> </Router> ) }
export default App
|
我們把 <Outlet/>
直接放到 /user
的 <Route>
上,意思就會是「請顯示 /user
的子路由」。接著我們又新增了一個 path=""
的子路由,這是什麼意思?
意思是當我在 /user
這個頁面時,會匹配到 /user""
這個子路由,然後顯示 <User />
。(有點饒口,你可能要多看幾次會比較好理解)
所以經過這樣設計後,就可以實現在 /user/profile
的時候不會出現導覽列,只會有 <Profile />
的內容。
總之這邊只是想介紹一下這種做法,我對這種巢狀路由的操作還蠻陌生的,所以才特別記錄下來。
這邊也附上原始碼,有需要都可以去看看。
巢狀結構的優化
假設我有一個管理系統的網頁,路由結構如下:
- /system
- /system/groupA
- /system/groupA
- /ststem/groupA/add
- /system/groupA/:groupCode
然後 group 可能有很多個,例如 A、B、C 等等,那我可能就會這樣設計:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <Router> <Routes> {/* /system */} <Route path='system' element={<Outlet />}> <Route path='' element={<SystemWelcom />} /> {/* /system/groupA */} <Route path='groupA' element={<Outlet />}> <Route path='' element={<GroupAList />} /> <Route path=':groupCode/*' element={<GroupALDetail />} /> <Route path='add/*' element={<GroupAAdd />} /> <Route path='*' element={<GroupAList />} /> </Route> {/* /system/groupB */} <Route path='groupB' element={<Outlet />}> <Route path='' element={<GroupBList />} /> <Route path=':groupCode/*' element={<GroupBLDetail />} /> <Route path='add/*' element={<GroupBAdd />} /> <Route path='*' element={<GroupBList />} /> </Route> {/* /system/groupC */} <Route path='groupC' element={<Outlet />}> <Route path='' element={<GroupCList />} /> <Route path=':groupCode/*' element={<GroupCLDetail />} /> <Route path='add/*' element={<GroupCAdd />} /> <Route path='*' element={<GroupCList />} /> </Route> </Route> </Routes> </Router>
|
附註:如果這樣看有點抽象的話,我有寫一個簡單的範例,可以到這邊參考。
雖然這樣子寫沒什麼問題,但其實利用下面這種技巧可以寫更簡潔:
1 2 3 4 5 6 7 8 9 10
| <Router> <Routes> <Route path='system' element={<Outlet />}> <Route path='' element={<SystemWelcom />} /> <Route path='groupA/*' element={<GroupA />} /> <Route path='groupB/*' element={<GroupB />} /> <Route path='groupC/*' element={<GroupC />} /> </Route> </Routes> </Router>
|
這邊的技巧在於:
groupA/*
後面的 /*
groupA/*
後面的 /*
groupA/*
後面的 /*
因為我研究了蠻久才發現,所以多說幾次(X
你可能會想說用 /groupA
不行嗎?為什麼要 /groupA/*
?
絕對不行
這是因為 /groupA
的意思是「路徑必須是一字不差的 /groupA
才算匹配」,所以當網址 /groupA/add
或 /groupA/list
時,都會被當作不匹配,也就不會顯示 <GroupA />
這個元件。
但 /groupA/*
就不一樣了,/*
的意思是後面接什麼都可以,所以你要 /groupA/123
還是 /groupA/321
都無所謂,全部都會匹配成功,這個就是我們要的效果。
最後,你可能會好奇 <groupA />
是什麼東西?這邊就順便說一下。
這邊是利用這樣的資料夾結構:
- GroupA
- List.tsx
- Detail.tsx
- Add.tsx
- index.tsx
看到 index
的第一眼應該就能猜到一件事,就是只要看這支檔案就能知道這在幹嘛了,所以我們來看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React from 'react' import { Routes, Route } from 'react-router-dom' import List from './List' import Detail from './Detail' import Add from './Add'
const GroupA: React.FC = () => { return ( <Routes> <Route path='' element={<List />} /> <Route path='add' element={<Add />} /> <Route path=':groupCode' element={<Detail />} /> </Routes> ) }
export default GroupA
|
就是把對應的子路由和元件引入後在輸出而已,沒有你想的那麼複雜。
好啦,其實是我覺得要解釋很詳細的話會花很多時間,所以如果真的想研究的話,建議到這邊看原始碼比較快。
一個當初踩到的雷
如果一個 Component 有用到 <Link />
或是 useNavigate
之類的東西的話,那它一定要放在 <Router />
裡面,不然就會噴這個錯誤:
當初被這個雷卡很久,想說我根本沒用到 useHref
這個 hook 阿,真是的 QQ。
詳細可以參考這裡:https://stackoverflow.com/questions/70220413/error-usehref-may-be-used-only-in-the-context-of-a-router-component-it-wor
參考資料