React 學習筆記(3) - Router
===

###### tags: `React`, `React router`
---
## Router
- 安裝 React router
- `cd location where you want to place the project` ex: cd desktop
- 打開 終端機並輸入 `npx create-react-app <project name>`
- 安裝完畢後到專案資料夾內 `cd <project>`
- 安裝 react router (這裏用的版本是 5.1)

- 打開 VScode 及專案資料夾
- 在 `src` 資料夾內建立一個 `pages` 的資料夾,以及三個分頁 `Home.js`, `About.js`, `Contact.js`

- 開啟 VScod,先設定每一頁的基本樣式
```javascript=
// Home.js
import React from "react";
export default function Home() {
return (
<div>
<h2>Homepage</h2>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus
consequatur doloremque ipsam impedit reiciendis, cupiditate, est magnam
aut laboriosam vitae beatae distinctio ullam ipsa animi, in iusto
debitis assumenda fugit!
</p>
</div>
);
}
// About.js
import React from 'react';
export default function About() {
return (
<div>
<h2>About</h2>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus
consequatur doloremque ipsam impedit reiciendis, cupiditate, est magnam
aut laboriosam vitae beatae distinctio ullam ipsa animi, in iusto
debitis assumenda fugit!
</p>
</div>
);
}
// Contact.js
import React from 'react';
export default function Contact() {
return (
<div>
<h2>Contact</h2>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus
consequatur doloremque ipsam impedit reiciendis, cupiditate, est magnam
aut laboriosam vitae beatae distinctio ullam ipsa animi, in iusto
debitis assumenda fugit!
</p>
</div>
);
}
```
- 打開 `App.js` 引入 `BrowserRouter`、 `Route`、 以及三個分頁
```javascript=
// App.js
import { BrowserRouter, Route } from "react-router-dom";
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
// 使用 <BrowserRouter> 標籤包裹
function App() {
return (
<div className="App">
<BrowserRouter>
<nav>
<h1>My Articles</h1>
</nav>
<Route path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/contact">
<Contact />
</Route>
</BrowserRouter>
</div>
);
}
export default App;
```
- 在終端機輸入 `npm run start` 開啟本地端伺服器,試著在埠號後面加上 / 然後輸入 about 或者 contact,如果有出現相關的內容就代表有正確使用到 router

### Switch & exact
- 上面我們看到出來的畫面,雖然我們在網址後面加上 /contact,但實際上出來的畫面卻有 Homepage,這是為什麼呢?
- 因為我們沒有明確告知 React 要顯示哪一個網頁,因此它會逐一對照路徑,基本的順序是:
- 元件內是否有 `/` -> 有 -> 呈現出來 -> Homepage
- 輸入 `/contact` -> 有這個路徑嗎? -> 有 -> 呈現出來 -> Homepage -> contact
- 因為在 contact 之前,是 `/`,因此 Homepage 會先跑出來,接著才會是 contact
- 使用 Switch 來呈現單一化面,為了避免上述問題,我們引入 Switch
```javascript=
// app.js
import { BrowserRouter, Route, Switch } from "react-router-dom";
// Switch 包裹 <Route>
<Switch>
<Route path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/contact">
<Contact />
</Route>
</Switch>
```
- 這次為什麼是 Homepage 顯示在畫面呢?明明已經輸入 /contact 了?

- 主要因為即使我們已經引入 Switch,但由於我們的路徑都有 `/`,所以不論是 About 或是 Contact,都會因為 `/` 而先顯示 Homepage 的畫面
- 為了解決上述問題,我們必須使用 `exact` 來告知 React 只有 `/` 才需要顯示 Homepage 的畫面,其餘不要
```javascript=
//App.js
<Switch>
// 加上 exact
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/contact">
<Contact />
</Route>
</Switch>
```

### Links & NavLinks
- 試著把三個分頁放進導覽列中,有三種方式,我們分別來看看這三種的呈現方式
- 使用 `<a>`
- 使用 `<Link>`
- 使用 `<NavLink>`
```javascript=
// App.js
// 使用 <a>
<nav>
<h1>My Articles</h1>
<a href='/' >Home</a>
<a href='/about' >About</a>
<a href='/contact' >Contact</a>
</nav>
```
- 使用 `<a>` 的方式,會在使用者每一次點擊時,React 會重新抓取資料


- 使用 `<Link>`,僅需要在網頁初始載入時抓取資料,不會在點擊其他頁面時再次抓取資料,但缺點是它們無法透過 active 狀態來表現使用者點擊狀態
```javascript=
// App.js
<nav>
<h1>My Articles</h1>
<Link to='/' >Home</Link>
<Link to='/about' >About</Link>
<Link to='/contact' >Contact</Link>
</nav>
```
- 使用 `<NavLink>` 除了可以如同 `<Link>` 一樣,不需要再額外抓取資料,它會在使用者點擊時,主動加上 active 的 class name
```javascript=
// App.js
<nav>
<h1>My Articles</h1>
<NavLink to='/' >Home</NavLink>
<NavLink to='/about' >About</NavLink>
<NavLink to='/contact' >Contact</NavLink>
</nav>
```
```css=
nav a.active {
color: deeppink;
}
```
- 使用`<NavLink>` 雖然可以主動地為被點擊的分頁加上 active,但會遇到類似之前遇過的問題,React 在過濾路徑時,看到 `/` 以及 `/contact` 會認為這些都是需要被 active 的,因此兩邊都自動加上 active class name,解決方式一樣,我們在 `/` 路徑這邊加上 `exact`

```javascript=
// App.js
<nav>
<h1>My Articles</h1>
<NavLink exact to='/' >Home</NavLink>
<NavLink to='/about' >About</NavLink>
<NavLink to='/contact' >Contact</NavLink>
</nav>
```

---
### Fetch Data
- 使用之前建立過的 `useFetch.js` 來 fetch data
```javascript=
import { useState, useEffect } from "react"
export const useFetch = (url) => {
const [data, setData] = useState(null)
// 建立一個提醒來告知使用者資料載入中
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
// 假設介面有隱藏的按鈕,AbortController()
// 可以正確關閉在背景程式中仍在運作的 fetch 來避免 console 出現錯誤
const controller = new AbortController()
const fetchData = async () => {
// 在載入資料前,setIsPending === true
// 在子元件中使用
setIsPending(true)
try {
const res = await fetch(url, { signal: controller.signal })
if(!res.ok) {
throw new Error(res.statusText)
}
const data = await res.json()
setIsPending(false)
setData(data)
setError(null)
} catch (err) {
if (err.name === "AbortError") {
console.log("the fetch was aborted")
} else {
setIsPending(false)
setError('Could not fetch the data')
}
}
}
fetchData()
return () => {
controller.abort()
}
}, [url])
return { data, isPending, error }
}
```
```javascript=
// Home.js
// 引入 useFetch custome hook
import { useFetch } from "../hooks/useFetch";
// 可使用 useFetch 裡面的資料
// 這裏的 data 我們將它命名為 article
export default function Home() {
const { data: articles, isPending, error } = useFetch("http://localhost:3000/articles");
return (
<div className="home">
<h2>Homepage</h2>
// 如果 fetch 仍在進行,顯示 Loading...
{isPending && <div>Loading...</div>}
// 如果 fetch 不到資料或是其他錯誤,顯示錯誤訊息
{error && <div>{error}</div>}
// 這邊需要注意的是,article 必須先為 true,否則沒有資料無法進行 loop
{articles && articles.map((article) => (
// 透過 useFetch,我們可以取得 json 內的資料,包括 id, author, title等
<div key={article.id} className="card">
<h3>{article.title}</h3>
<p>{article.author}</p>
// 不直接使用 articel.body 來顯示文章
// 透過建立 Read more 連結讓使用者點擊後看到文章
// 這邊的 url 需要透過文章 id 作為路徑
// 必須使用 template iteral,會產生如 /articles/2 的連結
<Link to={`/articles/${article.id}`}>Read more...</Link>
</div>
))}
</div>
);
}
```
---
- json 範例

- 首頁呈現畫面

---
### Router parameter
- 新建立 `Article.js` 元件,引入 useParams
- 在 `App.js` 上新增 `<Article>` 的路徑
- 引入 `useParams`,參考資料 [API Reference](https://reactrouter.com/docs/en/v6/api#useparams)The useParams hook returns an object of key/value pairs of the dynamic params from the current URL that were matched by the `<Route path>` Child routes inherit all params from their parent routes.
```javascript=
// App.js
// 一樣需要包裹在 <BrowserRouter></BrowserRouter>內
// 用 id 作為分頁路徑的方式如下 `/路徑名稱/:<id or 序號等>`
<Route path="/articles/:id">
<Article />
</Route>
```
```javascript=
// Article.js
// 引入 useParams, useFetch
import { useParams } from "react-router-dom";
import { useFetch } from "../hooks/useFetch";
export default function Article() {
// 抓取在 App.js 內設定的路徑
const { id } = useParams();
const url = "http://localhost:3000/articles/" + id;
const { data: article, isPending, error } = useFetch(url);
return (
<div>
{isPending && <div>Loading...</div>}
{error && <div>{error}</div>}
{article && (
<div>
<h2>{article.title}</h2>
<p>By {article.author}</p>
<p>{article.body}</p>
</div>
)}
</div>
);
}
```
- 上述會發生一個狀況,就是當使用者輸入不存在的路徑,例如:/articles/6,頁面會呈現錯誤訊息 `Could not fetch the data`,但這不是我們希望的,我們希望過一秒或兩秒將使用者導到首頁,這時候就會需要 `useHistory`,可參考:[useHistory](https://v5.reactrouter.com/web/api/Hooks/usehistory),[react-router: useHistory, useLocation and useParams](https://dev.to/raaynaldo/react-router-usehistory-uselocation-and-useparams-10cd)
```javascript=
// Article.js
// 引入 useHistory
import { useParams, useHistory } from "react-router-dom";
export default function Article() {
const { id } = useParams();
const url = "http://localhost:3000/articles/" + id;
const { data: article, isPending, error } = useFetch(url);
// useHistory 會回傳物件,其中有 NavLink 等等的
const history = useHistory();
// 為了使網頁可以確認是否有錯誤,使用 useEffect,如果發生錯誤就導回到首頁
useEffect(() => {
if (error) {
setTimeout(() => {
history.push("/");
}, 2000);
}
// 將 history 加入到相依性內,因為 history 存在於 useEffect 的外部
}, [error, history]);
return (
<div>
{isPending && <div>Loading...</div>}
{error && <div>{error}</div>}
{article && (
<div>
<h2>{article.title}</h2>
<p>By {article.author}</p>
<p>{article.body}</p>
</div>
)}
</div>
);
}
```
- 點擊首頁第一個文章的 Read more,導到 id=1 的文章分頁

---
### Redirect component
- 有時候使用者如果輸入不存在的路徑,例如:"/articles/food",可以使用 `Redirect` 將使用者導到首頁
```javascript=
// App.js
// 引入 Redirect
import { BrowserRouter, Route, Switch, NavLink, Redirect } from "react-router-dom";
// 將 Redirect 放置於其他 Route 的最底層
// match everything, including unknown path
<Route path="*">
// Redirect user to homepage
<Redirect to="/" />
</Route>
```
---
### Query parameter
- 獲取網址中的參數(問號後面),例如:"/articles/user?name=jimmy"
- 使用 `useLocation` 和 URLSearchParams,可以參考[使用 JavaScript 解析網址與處理網址中的參數](https://pjchender.blogspot.com/2018/08/js-javascript-url-parameters.html)以及[URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)
```javascript=
// Contact.js
// 引入 useLocation 來取得網址中的參數
import { useLocation } from "react-router-dom";
export default function Contact() {
const queryStr = useLocation().search;
// 這裏會顯示在網址中輸入的任何參數
console.log(queryStr);
return (
<div>
<h2>Contact</h2>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus
consequatur doloremque ipsam impedit reiciendis, cupiditate, est magnam
aut laboriosam vitae beatae distinctio ullam ipsa animi, in iusto
debitis assumenda fugit!
</p>
</div>
);
}
```
- 當在網址中輸入 ?name=jimmy 時,console 也會將這段參數顯示出來

```javascript=
// Contact.js
// 透過 URLSearchParams 建構式來建構一個 URLSearchParams 的 instance
const queryParams = new URLSearchParams(queryStr);
// 可以確認 queryParams 是不是 URLSearchParams()建構出的 instance
console.log(queryParams instanceOf URLSearchParams) // true
// queryParams 繼承 URLSearchParams 內的許多方法,其中一項為 get()
const name = queryParams.get("name")
console.log(name) // jimmy
```
```javascript=
// Contact.js
// 透過上述我們取得了網址中的參數
import { useLocation } from "react-router-dom";
export default function Contact() {
const queryStr = useLocation().search;
//console.log(queryStr);
const queryParams = new URLSearchParams(queryStr);
console.log(queryParams instanceof URLSearchParams);
const name = queryParams.get("name");
//console.log(name);
return (
<div>
<h2>Hello, {name}, that's chat...</h2>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus
consequatur doloremque ipsam impedit reiciendis, cupiditate, est magnam
aut laboriosam vitae beatae distinctio ullam ipsa animi, in iusto
debitis assumenda fugit!
</p>
</div>
);
}
```
- 可以把參數動態地渲染到畫面上

---
### react-router 6 內容更新
- `Switch` 改成 `Routes`
- 原本使用 `Route` 包覆分頁,更改成以下:
```javascript=
// Old way
import { BrowserRouter, Route, Switch } from "react-router-dom";
<Route path="/">
<Home />
<Route />
// v6
import { BrowserRouter, Route, Routes } from "react-router-dom";
<Routes>
<Route exact path="/" element={<HomePage />} />
</Routes>
```