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

如果要安裝特定版本

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

我們參考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。(詳參: ThemesPalette )

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>

)


}

作業

  1. 試著增加新頁面以及route

  2. 改用Tab 或其他的介面

  3. 試著設定theme的其他相關顏色 (詳參: PaletteHTML Color Picker)

常見問題

Q:如何利用javascript直接呼叫某個route?

A:利用react router 5的useHistory

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

Material-UI