Routing
Routing
2021/10/19 (更新內容)
2021/11/16 (更新內容到第6版)
2021/11/23 (更新內容到第6版)
2021/12/01 (新增連結)
React Router
基本上,react只會有單一的入口,也就是一定從index.js開始執行,不像php那樣,可以直接執行任何的php檔案。所以,當我們的程式長大之後,就必須要靠router來幫忙了。假如,我們有兩個子功能,一個是ProductList,另一個是EmployeeList,怎麼把這兩個部分放在一起呢?
首先,先安裝react-router套件,目前最新的版本是6.0.2。(2021/11/16)
npm install react-router-dom
** 注意 ** 6.0有一些重大的改變: Upgrading from v5
Notes
如果要安裝特定版本
npm install react-router-dom@5.3.0
Route設定
Router有兩種,一種是BrowserRouter,另一種是HashRouter。主要的差別在於採用BrowserRouter的話,採用的是標準的URL,有些伺服器(如:apache)就必須更動設定才有辦法使用標準的URL,如果,採用HashRouter,則是利用hash,這樣的寫法沒有伺服器的設定問題。
如果想要保留切換Router的彈性,可以利用
import {BrowserRouter as Router} from 'react-router-dom';
未來想切換為HashRouter,就只要改這裡
import {HashRouter as Router} from 'react-router-dom';
Router (BrowserRouter)下有Routes (v6)。
<Router>
<Routes>
</Routes>
</Router>
在Routes中設定Route,利用Route設定哪個路徑 (path)會對應到哪個元件(element)。
<Router>
<Routes>
<Route path="/" element={<Main/>}/>
<Route path="/product" element={<ProductList/>}/>
<Route path="/employee" element={<EmployeeList/>}/>
</Routes>
</Router>
在v6,把component改成element,好處是,可以直接設定props。
React Router v5 的設定
Router (BrowserRouter)下可以有Switch (v5)。
<Router>
<Switch>
</Switch>
</Router>
在Switch中設定Route,利用Route設定哪個路徑 (path)會對應到哪個元件(component)。
<Router>
<Switch>
<Route path="/product" component={ProductList}/>
<Route path="/employee" component={EmployeeList}/>
<Route path="/" component={Main}/>
</Switch>
</Router>
Switch的作用是只要有任何一個Route對應到path,就不會往下對應了,這樣會讓速度加快。所以,Route的順序很重要,例如,path="/"的Route放在path="/student"的Route之前,就會先使用path="/"的Route,那path="/student"的route就對應不到了。解決方法有:
path="/"的Route放在path="/student"的Route之後
在path前增加exact
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
//import './index.css';
//import App from './App';
import Main from './ui/Main.js';
import EmployeeList from './employee/EmployeeList.js';
import ProductList from './product/ProductList.js';
ReactDOM.render(
<React.StrictMode>
<Router>
<Switch>
<Route path="/product" component={ProductList}/>
<Route path="/employee" component={EmployeeList}/>
<Route path="/" component={Main}/>
</Switch>
</Router>
</React.StrictMode>
, document.getElementById('root'));
另一種解決方法是在path="/"前加exact,那就不會把"/"跟"/student"及"/employee"弄混了。
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
//import './index.css';
//import App from './App';
import Main from './ui/Main.js';
import EmployeeList from './employee/EmployeeList.js';
import ProductList from './product/ProductList.js';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<Router>
<Switch>
<Route exact path="/" component={Main}/>
<Route path="/product" component={ProductList}/>
<Route path="/employee" component={EmployeeList}/>
</Switch>
</Router>
</React.StrictMode>
, document.getElementById('root'));
**未來在react router 6之後,所有的path都預設exact,以後就沒有這個困擾了。
Link設定
接下來就可以利用"Link"去連接到這些路徑,其實跟<a> </a>的作用類似,只是Link是client side routing,<a> </a>是server side routing。(詳參: Basic routing in React using React Router 之 Why do we need Link component, why not a HTML anchor tag with href?)
src/ui/Main.js
import React from 'react';
import {Link} from 'react-router-dom';
import { Box, Button } from '@mui/material';
export default function Main() {
return (
<Box>
<Button><Link to='/employee'>LINK to employee</Link></Button>
<Button><Link to='/product'>LINK to product</Link></Button>
</Box>
)
}
src/product/ProductList.js及src/product/ProductAdd.js請參考Form 。
【Day.16】React入門 - 想要分頁? react-router-dom (內容為react router 5)
我們參考ProductList,新增了一個EmployeeList
src/employee/EmployeeList.js
import React, {useState} from 'react';
import { Box, List, ListItem, ListItemText} from '@mui/material';
export default function EmployeeList() {
const [employees] = useState([
{id:"0", name:"Ben", department:"IT"},
{id:"1", name:"Rich", department:"Marketing"},
{id:"2", name:"Ruby", department:"Management"},
{id:"3", name:"Judy", department:"IT"},
{id:"4", name:"Rain", department:"IT"}
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleListItemClick = (index) => {
setSelectedIndex(index);
};
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}} >
<List subheader="Employee list" aria-label="employee list">
{employees.map((employee, index) =>
<ListItem divider key={index} onClick={()=>handleListItemClick(index)} selected={selectedIndex === index}>
<ListItemText primary={employee.name} secondary={"@"+employee.department}></ListItemText>
</ListItem>)}
</List>
</Box>
);
}
其實設定好router之後,也可以直接用url啟動這兩個app,例如:
http://localhost:3000/employee
Material-UI (MUI)
我們一般會結合route與其他的UI元件,例如,Material-UI的AppBar。
首先,先新增一個AppMenu,在AppMenu裡新增AppBar。
<AppBar >
</AppBar>
要讓Link的style跟著AppBar,就把Link包在Button的component裡,並設定顏色是繼承上一層。
<Button component={Link} to='/product' color="inherit">product</Button>
src/ui/Main.js
import React from 'react';
import {Link} from 'react-router-dom';
import { AppBar, Button } from '@mui/material';
export default function Main() {
return (
<AppBar >
<Button component={Link} to='/product' color="inherit">product</Button>
<Button component={Link} to='/employee' color="inherit">employee</Button>
</AppBar>
)
}
會發現AppBar會列了兩個Button,但卻是上下排,跟一般的AppBar不太一樣,這時候,我們就需要加上Toolbar。
<AppBar >
<Toolbar>
<Button component={Link} to='/product' color="inherit">product</Button>
<Button component={Link} to='/employee' color="inherit">employee</Button>
</Toolbar>
</AppBar>
如果要每個頁面都要有AppBar,我們先將AppBar獨立為:AppMenu
src/ui/AppMenu.js
import React from 'react';
import {Link} from 'react-router-dom';
import { AppBar, Button, Toolbar } from '@mui/material';
export default function AppMenu() {
return (
<AppBar>
<Toolbar>
<Button component={Link} to='/product' color="inherit">product</Button>
<Button component={Link} to='/employee' color="inherit">employee</Button>
</Toolbar>
</AppBar>
)
}
在每一頁都加入AppMenu
src/ui/Main.js
import React from 'react';
import {Link} from 'react-router-dom';
import AppMenu from './AppMenu';
import { Box,Button} from '@mui/material';
export default function Main() {
return (
<Box>
<AppMenu/>
<Button component={Link} to='/product' varient="primary">product</Button>
<Button component={Link} to='/employee' varient="primary">employee</Button>
</Box>
)
}
這時候,會發現List被AppMenu遮住了,調整一下AppMenu: (不適用於IE11)
<AppBar position="sticky">
src/ui/AppMenu.js
import React from 'react';
import {Link} from 'react-router-dom';
import { AppBar, Button, Toolbar } from '@mui/material';
export default function AppMenu() {
return (
<AppBar position="sticky">
<Toolbar>
<Button component={Link} to='/product' color="inherit">product</Button>
<Button component={Link} to='/employee' color="inherit">employee</Button>
</Toolbar>
</AppBar>
)
}
將AppMenu放到EmployeeList.js。
import React, {useState} from 'react';
import { Box, List, ListItem, ListItemText} from '@mui/material';
import AppMenu from '../ui/AppMenu';
export default function EmployeeList() {
const [employees] = useState([
{id:"0", name:"Ben", department:"IT"},
{id:"1", name:"Rich", department:"Marketing"},
{id:"2", name:"Ruby", department:"Management"},
{id:"3", name:"Judy", department:"IT"},
{id:"4", name:"Rain", department:"IT"}
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleListItemClick = (index) => {
setSelectedIndex(index);
};
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}} >
<AppMenu/>
<List subheader="Employee list" aria-label="employee list">
{employees.map((employee, index) =>
<ListItem divider key={index} onClick={()=>handleListItemClick(index)} selected={selectedIndex === index}>
<ListItemText primary={employee.name} secondary={"@"+employee.department}></ListItemText>
</ListItem>)}
</List>
</Box>
);
}
將AppMenu放到ProductList.js。
import React from 'react';
import { Box, List, ListItem, ListItemText } from '@mui/material';
import AppMenu from '../ui/AppMenu';
export default function ProductList() {
const products= [
{desc:"iPad", price:20000},
{desc:"iPhone X", price:30000},
];
return (
<Box sx={{
width: '100vw',
height: '100vh',
backgroundColor: 'background.paper',
color: 'black',
textAlign: 'left'
}}>
<AppMenu/>
<List subheader="Product list" aria-label="product list">
{products.map((product, index) =>
<ListItem divider key={index}>
<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>
</ListItem>)}
</List>
</Box>
);
}
NavLink
在react router裡可以使用NavLink,NavLink會去比對現在的頁面的位置,當位置為我們設定的連結,就會利用isActive套用對應的樣式
import React from 'react';
import {NavLink} from 'react-router-dom';
import { AppBar, Button, Toolbar } from '@mui/material';
export default function AppMenu() {
return (
<AppBar position="sticky">
<Toolbar>
<Button component={NavLink} to='/product'
style={({ isActive }) => (isActive? { color: 'black',backgroundColor: 'deepskyblue'}:
{ color: 'inherit',backgroundColor: 'inherit'} )}>Product</Button>
<Button component={NavLink} to='/employee'
style={({ isActive }) => (isActive? { color: 'black',backgroundColor: 'deepskyblue'}:
{ color: 'inherit',backgroundColor: 'inherit'} )}>Employee</Button>
</Toolbar>
</AppBar>
)
}
React Router v5 的NavLink
在react router裡可以使用NavLink,NavLink會去比對現在的頁面的位置,當位置為我們設定的連結,就會套用activeStyle
import React from 'react';
import {NavLink} from 'react-router-dom';
import { AppBar, Button, Toolbar } from '@mui/material';
export default function AppMenu() {
const activeStyle = { backgroundColor:"deepskyblue", color:"black" };
return (
<AppBar position="sticky">
<Toolbar>
<Button component={NavLink} to='/product' activeStyle={activeStyle} color="inherit">product</Button>
<Button component={NavLink} to='/employee' activeStyle={activeStyle} color="inherit">employee</Button>
</Toolbar>
</AppBar>
)
}
useNavigate
透過javascript呼叫頁面,可利用useNavigate:
import React from 'react';
//import {Link} from 'react-router-dom';
import { Box, Button } from '@mui/material';
import { useNavigate } from "react-router-dom";
import AppMenu from './AppMenu';
export default function Main() {
const navigate = useNavigate();
const handleClick = function (link) {
navigate(link);
}
return (
<Box>
<AppMenu/>
<Button variant="contained" color="primary" onClick={()=>handleClick("/product")}>Product</Button>
<Button variant="contained" color="secondary" onClick={()=>handleClick("/employee")}>Employee</Button>
</Box>
)
}
React Router v5 的useHistory
透過javascript呼叫頁面,可利用useHistory:
import React from 'react';
import { Box,Button} from '@mui/material';
import { useHistory } from "react-router-dom";
import AppMenu from './AppMenu';
export default function Main() {
const history = useHistory();
const handleClick = function (link) {
history.push(link);
}
return (
<Box>
<AppMenu/>
<Button variant="contained" color="primary" onClick={()=>handleClick("/product")}>Product</Button>
<Button variant="contained" color="secondary" onClick={()=>handleClick("/employee")}>Employee</Button>
</Box>
)
}
注意: 不可以寫成
<Button variant="contained" color="primary" onClick={handleClick("/product")}>Product</Button>
<Button variant="contained" color="secondary" onClick={handleClick("/employee")}>Employee</Button>
這樣寫的話,發生甚麼事? 為什麼?
Theme
先利用createTheme()去產生theme。(詳參: Themes、Palette )
const theme = createTheme({
palette: {
primary: {
main: '#f44336',
},
secondary: {
main: '#F5B7B1',
},
},
});
被ThemeProvider包括的元件就會使用客製的theme。
<ThemeProvider theme={theme}>
<Router>
<Switch>
<Route path="/product" component={ProductList}/>
<Route path="/employee" component={EmployeeList}/>
<Route path="/" component={Main}/>
</Switch>
</Router>
</ThemeProvider>
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
//import './index.css';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import reportWebVitals from './reportWebVitals';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import ProductList from './product/ProductList';
import EmployeeList from './employee/EmployeeList';
import Main from './ui/Main';
const theme = createTheme({
palette: {
primary: {
main: '#f44336',
},
secondary: {
main: '#F5B7B1',
},
},
});
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<Router>
<Switch>
<Route path="/product" component={ProductList}/>
<Route path="/employee" component={EmployeeList}/>
<Route path="/" component={Main}/>
</Switch>
</Router>
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
因為我們在使用顏色時,使用primary及secondary,我們就可以看到所有元件中只要是用到primary及secondary的顏色都跟著變了。
import React from 'react';
import { Box,Button} from '@mui/material';
import { useHistory } from "react-router-dom";
import AppMenu from './AppMenu';
export default function Main() {
const history = useHistory();
const handleClick = function (link) {
history.push(link);
}
return (
<Box>
<AppMenu/>
<Button variant="contained" color="primary" onClick={()=>handleClick("/product")}>Product</Button>
<Button variant="contained" color="secondary" onClick={()=>handleClick("/employee")}>Employee</Button>
</Box>
)
}
可以利用useTheme取得theme,這樣的好處是,整個系統的樣式可以透過ThemeProvider來管理,例如:
import React from 'react';
import {NavLink} from 'react-router-dom';
import { AppBar, Button, Toolbar } from '@mui/material';
import { useTheme } from '@mui/material/styles';
export default function AppMenu() {
const theme = useTheme();
const activeStyle = { backgroundColor:theme.palette.secondary.main, color:"black" };
return (
<AppBar position="sticky">
<Toolbar>
<Button component={NavLink} to='/product' activeStyle={activeStyle} color="inherit">product</Button>
<Button component={NavLink} to='/employee' activeStyle={activeStyle} color="inherit">employee</Button>
</Toolbar>
</AppBar>
)
}
作業
試著增加新頁面以及route
改用Tab 或其他的介面
試著設定theme的其他相關顏色 (詳參: Palette 、 HTML Color Picker)
常見問題
Q:如何利用javascript直接呼叫某個route?
A:利用react router 5的useHistory
useParams
useLocation
useHistory
useRouteMatch
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}
** 待補充
NavLink的運作方式其實就是把style變成變數,所以,如果不用NavLink,可以這樣:
import React from "react"
import { Route, Link } from "react-router-dom";
import { makeStyles } from '@material-ui/core/styles';
import { Button } from '@material-ui/core/';
/*------------ STYLE ------------*/
const useStyles = makeStyles({
bookMark:{
background: '#4A90E2', /*背景顏色#4A90E2*/
color: '#ffffff', /*字體顏色*/
textAlign: 'center', /*置中*/
fontSize: '24px', /*字體大小*/
height: '75px', /*按鈕高度*/
width: '220px', /*按鈕寬度*/
border:'none', /*取消框線*/
borderBottom:'solid 0px #00408B',
},
select_bookMark: {
//color: 'secondary',
background: '#4A90E2', /*背景顏色#4A90E2*/
color: '#ffffff', /*字體顏色*/
textAlign: 'center', /*置中*/
fontSize: '24px', /*字體大小*/
//height: '75px', /*按鈕高度*/
width: '220px', /*按鈕寬度*/
border:'none', /*取消框線*/
height: '70px', /*改變高度因為要扣底線的粗5px不然會凸出來*/
borderBottom:'solid 5px #00408B', /*設定下底線的樣式、粗細、顏色*/
}
});
/*------------------------------*/
export default function BookMark(props) {
//console.log(props);
const classes = useStyles();
return (
<Route exact path={props.to}
children={p => {
console.log(p);
//console.log(classes);
let cName = p.match?classes.select_bookMark:classes.bookMark;
return (
<Link to={props.to}>
<Button className={cName}>{props.name}</Button>
</Link>
)
}} />
)
}
常見問題
如何傳遞變數?
React Router 6:
Use the useNavigate hook:
const navigate = useNavigate();
navigate('/other-page', { state: { id: 7, color: 'green' } });
Then, you can access the state data in '/other-page' via the useLocation hook:
const {state} = useLocation();
const { id, color } = state; // Read values passed on state
參考資料
React Router
React Router 專題知識分享
Quick Start (ReactRouter.com)
React-router-dom | 原理解析: 解析 5.2 版本原始碼
<Routes> is the new <Switch>
<Route component> is replaced with <Route element>
No more exact and strict props
Relative paths and links
Easier Nested Routes
Future-ready useNavigate instead of useHistory
Bundle size — 28.4 kB to 10.2 kB