# React(MRWR)第 13 節:Making Navigation Reusable
> Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/)
`20240126Fri~20240131Wed.`
:::danger
:::
## 13-218. Traditional Browser Navigation
做一個導覽列在左側,讓我們可以透過導覽列看到各個我們之前實作的元件們。

底下是原生html(沒有react也沒有JS)的運作方式:



****
## 13-219. Theory of Navigation in React

****
## 13-220. Extracting the DropdownPage

App底下,會有4個元件,其中會顯示buttonpage、accordion page或是dropdownpage取決於使用者點擊何者,使我們的route導向何處,便顯示何者的元件
## 13-221. Answering Critical Questions

根據上圖來實作。
前情提要,目前的App.js檔案中已經被清空了:

### User types our address in
1. Always send back the index.html file
這件事情其實當我們使用create react app時就已經幫我們完成了。我們可以先打開dev tool看看network,並且只選擇doc來看,首先我們在`localhost`頁面的情況下,會發現即便App.js檔案中只return了個div,我們仍然可以得到一個response為完整的html,而這個html檔案便是`public/index.html`

`public/index.html`

這個時候若我們導向`localhost/asdfghjkl`(隨便打),會發生什麼事呢?我們會發現即便報錯404 not found,但點進去看他的response,可以發現他仍然回傳了上方提到的html檔案:

2. When app loads up, look at address bar and use it to decide what content to show
兩個問題:
* How do we look at the address bar?
* 重點在於「path」:

* path 是什麼?

* What part of it do we care about?
當我們到`localhost3000`的頁面底下,打開devtool,在console中輸入`window.location`可以看到一個Location 物件,其中有一個最重要的屬性名叫「pathname」,而他正好就是我們需要關注的重點!!!:


再來看一個案例,當我們現在在`localhost3000/dropdown`頁面底下,在console中輸入`window.location`,我們可以看到此時pathname為"/dropdown":


## 13-222. The PushState Function

一樣根據上圖來實作,上一節講完上圖中上半部份,這一節將從下半部份最後一個講起。
### User clicks a link or presses "back" button
1. Update address bar to trick the user into thinking they swapped pages
* 問題在於:How do we update the address bar?怎麼更換address bar中的path呢?
`>` 有兩個辦法可以改變address bar中的path,如下圖所示,但只有下方的pushState方法可以不讓整個頁面重整,而是只更新address bar,而pushState方法為瀏覽器提供的方法。

****
## 13-223. Handling Link Clicks
## 13-224. Handling Back:Forward Buttons
接續上一節剩餘部份。
### User clicks a link or presses "back" button
2. Stop the browser's default page-switching behavior!(不要再讓browser使整個頁面全部重整!)
3. Feagure out where the user was trying to go?
`>`2、3一起講,他們的問題有兩個:
(1)How do we detect a user clicking a link? 我們要如何偵測使用者點擊了連結?
基本上實作方式如下所示,針對下圖加以說明:
1. 我們會有一個Link元件,他會取得名為to的prop,這個名為to的prop即為使用者點擊並即將前往的address。
2. Link元件裡會有一個anchor元素,並且此a tag會擁有特別的onClick event handler。
3. 因此當使用者點擊該a tag,將觸發onClick中的handleClick函式。
4. handleClick函式中會接收event 物件,而函式裡頭最重要的便是先停止browser標準的navigation(即整個頁面全部重整),所以叫出`event.preventDefault()`

(2)How do we detect a user clicking a back or forward? 我們要如何偵測使用者點擊了返回鍵或下一頁的按鍵?
1. 利用觸發popstate event來達成目的,但前提是user目前所在的url是由pushState所新增上去的(pushState在[13-122](https://hackmd.io/nXgMTzFDRjC0jTku9qpJfg#13-222-The-PushState-Function)有提到)

所以這裡我們並不需要去叫出`event.preventDefault()`來停止browser預設重整全部頁面的行為了,因為這裡直接使用pushState,而pushState再進到不同routes時,並不會使browser整個頁面重整。
2. 之後我們便可以透過popstate event,來監聽該事件的pathname,取得使用者想去哪裡。
****
## 13-225. Navigation Context

這節會用到createContext,可以回到[React(MRWR)第 8 節: Communication Using the Context System](https://hackmd.io/6ukAhVytTra5YYvV65mhSw?view)複習一下。
**/context/navigation.js**
```javascript!
import {createContext} from 'react';
const NavigationContext = createContext();
function NavigationProvider({children}){
return(
<NavigationContext.Provider value={{}}>
{children}
</NavigationContext.Provider>
)
}
export { NavigationProvider };
export default NavigationContext;
```
**/index.js**
```javascript!
import "./index.css";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { NavigationProvider } from './context/navigation';
const el = document.getElementById("root");
const root = ReactDOM.createRoot(el);
root.render(
<NavigationProvider>
<App />
</NavigationProvider>
);
```
****
## 13-226. Listening to Forward and Back Clicks

****
## 13-227. Programmatic Navigation
以下是導覽列進行的一般情況,user點擊,就會馬上跳轉至目標頁面:

但有時候會遇到以下的情況,例如說一些銀行網站發現你已經5分鐘沒有對網頁進行任何動作,他會跳出一個訊息說:再過20秒將自動登出,且不只幫你自動登出,還會把你的頁面跳轉至其他頁面,可能是首頁之類的:

但其實以上兩者的概念都在於"navigate"。
****
實作想法如下:

實作程式碼如下:
```javascript!
import {createContext, useState, useEffect} from 'react';
const NavigationContext = createContext();
function NavigationProvider({children}){
const [currentPath, setCurrentPath] =useState(window.location.pahtname);
useEffect(() => {
const handler = () => {
setCurrentPath(window.location.pathname);
}
window.addEventListener("popstate", handler);
return () => {
window.removeEventListener("popstate", handler);
};
},[])
const navigate = (to) => {
window.history.pushState({}, '', to);
setCurrentPath(to)
}
return(
<NavigationContext.Provider value={{ currentPath, navigate }}>
{children}
</NavigationContext.Provider>
)
}
export { NavigationProvider };
export default NavigationContext;
```
****
## 13-228. A Link Component
想法:

關於context:

實作方式:

****
## 13-229. A Route Component
再來要關心的重點在於我們要show什麼內容到螢幕之上?
建立一個Route元件!
實作想法:

實作程式碼:
**Route.js**
```javascript!
import { useContext } from 'react';
import NavigationContext from '../context/navigation';
function Route({ path, children }){
const { currentPath } = useContext(NavigationContext);
if(currentPath === path){
return children;
}
return null;
}
export default Route;
```
**App.js**
```javascript!
import Link from "./components/Link";
import Route from "./components/Route";
import AccordionPage from "./pages/AccordionPage"
import DropdownPage from "./pages/DropdownPage"
function App(){
return(
<div>
<Link to="/accordion">Go to accordion</Link>
<Link to="/dropdown">Go to dropdown</Link>
<div>
<Route path="/accordion">
<AccordionPage />
</Route>
<Route path="/dropdown">
<DropdownPage />
</Route>
</div>
</div>
)
}
export default App;
```
****
## 13-230. Handling Control and Command Keys
原先Link.js中的 anchor ,當我們點擊連結同時按下ctrl鍵或mac中的cmd鍵時,是沒辦法跑出新視窗的,這是因為我們當時為了避免瀏覽器在我們點擊連結時,重整整個頁面,而設下了`event.preventDefault()`,這一節就是要來處理這個問題。
先看看原先的Link.js:
```javascript!
import { useContext } from "react";
import NavigationContext from "../context/navigation";
function Link({ to, children }){
const {navigate} = useContext(NavigationContext)
const handleClick = (event) => {
event.preventDefault();
navigate(to);
}
return <a onClick={handleClick}>{children}</a>
}
export default Link;
```
首先,先把anchor的href(念法為h-ref)加上去,那這個連結要連去哪呢?就是prop to傳進來的值。
```javascript!
return <a href={to} onClick={handleClick}>{children}</a>
```
接著來看看當我們按下這個anchor後,觸發了onClick,並執行了handleClick,我們試著在handleClick裡面印出event 物件,看看這個event物件有什麼東西(這個event物件就是現在觸發的onClick event)
可以看到一大串內容,其中的ctrlKey與metaKey就是我們要的屬性,這兩個分別是針對windows用戶與mac用戶,若這兩個屬性為true,代表使用者按下了ctrl鍵或mac中的cmd鍵:

因此,我們便可以設置,假如ctrlKey與metaKey這兩者其中之一為true,我就不要用設置好的navigate(to)到新頁面,而是使用原始預設的方法,把連結內容用新視窗開啟。
****
## 13-232. Custom Navigation Hook

****
## 13-233. Adding a Sidebar Component
用map render 重複的component要記得加上key prop。
****
## 13-234. Highlighting the Active Link
問題:
如何使當前所在頁面的Link變成粗體?
實作想法:

實作程式碼:
**Link.js**
```javascript!
import classNames from "classnames";
import useNavigation from "../hooks/use-navigation";
function Link({ to, children, className, activeClassName }){
const {navigate, currentPath} = useNavigation();
const classes = classNames(
"text-blue-500",
className,
to === currentPath && activeClassName
);
const handleClick = (event) => {
if(event.metaKey || event.ctrlKey){
return;
}
event.preventDefault();
navigate(to);
}
return <a className={classes} href={to} onClick={handleClick}>{children}</a>
}
export default Link;
```
**Sidebar.js**
```javascript!
import Link from "./Link";
function Sidebar() {
const links = [
{ label: "Dropdown", path: "/" },
{ label: "Accordion", path: "/accordion" },
{ label: "Buttons", path: "/buttons" }
];
const renderedLinks = links.map((link) => {
return (
<Link
key={link.label}
to={link.path}
className="mb-3"
activeClassName="font-bold border-l-4 border-blue-500 pl-2"
>
{link.label}
</Link>
)
})
return (
<div className="sticky top-0 over-flow-y-scroll flex flex-col items-start">
{renderedLinks}
</div>
)
}
export default Sidebar;
```
****
## 13-235. Navigation Wrapup
其他實作router的函式庫,可以直接看他們各自的官方文件。
