---
# System prepended metadata

title: React(MRWR)第 13 節：Making Navigation Reusable
tags: [React, Udemy]

---

# 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
做一個導覽列在左側，讓我們可以透過導覽列看到各個我們之前實作的元件們。

![2024-01-26 12-59-09 的螢幕擷圖](https://hackmd.io/_uploads/SygVn2g5p.png)

底下是原生html（沒有react也沒有JS）的運作方式：
![2024-01-26 13-01-42 的螢幕擷圖](https://hackmd.io/_uploads/H1WC3ng56.png)

![2024-01-26 13-05-02 的螢幕擷圖](https://hackmd.io/_uploads/rJZ9T2xcp.png)

![2024-01-26 13-11-31 的螢幕擷圖](https://hackmd.io/_uploads/ryJQk6e9p.png)

****

## 13-219. Theory of Navigation in React
![2024-01-29 20-55-50 的螢幕擷圖](https://hackmd.io/_uploads/H1f9gmHqp.png)

****

## 13-220. Extracting the DropdownPage
![2024-01-29 20-58-06 的螢幕擷圖](https://hackmd.io/_uploads/rJplbmH5T.png)

App底下，會有4個元件，其中會顯示buttonpage、accordion page或是dropdownpage取決於使用者點擊何者，使我們的route導向何處，便顯示何者的元件

## 13-221. Answering Critical Questions
![2024-01-29 20-55-50 的螢幕擷圖](https://hackmd.io/_uploads/H1f9gmHqp.png)

根據上圖來實作。
前情提要，目前的App.js檔案中已經被清空了：
![2024-01-29 21-08-36 的螢幕擷圖](https://hackmd.io/_uploads/Byr3QXB9a.png)


### 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`
    ![2024-01-29 21-15-50 的螢幕擷圖](https://hackmd.io/_uploads/ryTBHmrqp.png)
    `public/index.html`
    ![2024-01-29 21-13-42 的螢幕擷圖](https://hackmd.io/_uploads/HJvcEQr56.png)
    這個時候若我們導向`localhost/asdfghjkl`（隨便打），會發生什麼事呢？我們會發現即便報錯404 not found，但點進去看他的response，可以發現他仍然回傳了上方提到的html檔案：
    ![2024-01-29 21-15-36 的螢幕擷圖](https://hackmd.io/_uploads/HkBSSmr5a.png)


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」：
            ![2024-01-29 21-21-51 的螢幕擷圖](https://hackmd.io/_uploads/Sk6FLQHca.png)
        * path 是什麼？
            ![2024-01-29 21-23-22 的螢幕擷圖](https://hackmd.io/_uploads/S1aAIQH9T.png)
    * What part of it do we care about?
        當我們到`localhost3000`的頁面底下，打開devtool，在console中輸入`window.location`可以看到一個Location 物件，其中有一個最重要的屬性名叫「pathname」，而他正好就是我們需要關注的重點！！！：
        ![2024-01-29 21-30-18 的螢幕擷圖](https://hackmd.io/_uploads/BkaOOQBcT.png)
        ![2024-01-29 21-26-20 的螢幕擷圖](https://hackmd.io/_uploads/Bk1qwXS9a.png)
        再來看一個案例，當我們現在在`localhost3000/dropdown`頁面底下，在console中輸入`window.location`，我們可以看到此時pathname為"/dropdown"：
        ![2024-01-29 21-29-40 的螢幕擷圖](https://hackmd.io/_uploads/rJILuXr5a.png)
        ![2024-01-29 21-29-02 的螢幕擷圖](https://hackmd.io/_uploads/HJQVdQBqp.png)

## 13-222. The PushState Function
![2024-01-29 20-55-50 的螢幕擷圖](https://hackmd.io/_uploads/H1f9gmHqp.png)

一樣根據上圖來實作，上一節講完上圖中上半部份，這一節將從下半部份最後一個講起。

### 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方法為瀏覽器提供的方法。
        ![2024-01-29 21-39-02 的螢幕擷圖](https://hackmd.io/_uploads/ByQ957Bcp.png)

****

## 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()`
    ![2024-01-29 21-50-49 的螢幕擷圖](https://hackmd.io/_uploads/rk0BTXr9p.png)
    
    （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)有提到）
        ![2024-01-29 22-05-35 的螢幕擷圖](https://hackmd.io/_uploads/B1Pal4H5p.png)
        所以這裡我們並不需要去叫出`event.preventDefault()`來停止browser預設重整全部頁面的行為了，因為這裡直接使用pushState，而pushState再進到不同routes時，並不會使browser整個頁面重整。
    2. 之後我們便可以透過popstate event，來監聽該事件的pathname，取得使用者想去哪裡。

    
****

## 13-225. Navigation Context
![2024-01-30 11-34-58 的螢幕擷圖](https://hackmd.io/_uploads/S1ttCkU9T.png)


這節會用到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
![image](https://hackmd.io/_uploads/rJyRikUq6.png)

****

## 13-227. Programmatic Navigation
以下是導覽列進行的一般情況，user點擊，就會馬上跳轉至目標頁面：
![2024-01-30 11-26-18 的螢幕擷圖](https://hackmd.io/_uploads/Sy232JIcp.png)

但有時候會遇到以下的情況，例如說一些銀行網站發現你已經5分鐘沒有對網頁進行任何動作，他會跳出一個訊息說：再過20秒將自動登出，且不只幫你自動登出，還會把你的頁面跳轉至其他頁面，可能是首頁之類的：
![2024-01-30 11-26-08 的螢幕擷圖](https://hackmd.io/_uploads/BJyfTyUqp.png)

但其實以上兩者的概念都在於"navigate"。

＊＊＊＊

實作想法如下：
![image](https://hackmd.io/_uploads/S1CkzxI56.png)

實作程式碼如下：
```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
想法：
![image](https://hackmd.io/_uploads/SkLA6gU96.png)

關於context：
![image](https://hackmd.io/_uploads/S1JyRlIqT.png)

實作方式：
![image](https://hackmd.io/_uploads/H1qjTgI96.png)

****

## 13-229. A Route Component
再來要關心的重點在於我們要show什麼內容到螢幕之上？
建立一個Route元件！

實作想法：
![image](https://hackmd.io/_uploads/HkCYzNPq6.png)

實作程式碼：
**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鍵：
![image](https://hackmd.io/_uploads/SyQsYVP9a.png)

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

****

## 13-232. Custom Navigation Hook
![image](https://hackmd.io/_uploads/H1tlBrv5a.png)

****

## 13-233. Adding a Sidebar Component
用map render 重複的component要記得加上key prop。

****

## 13-234. Highlighting the Active Link
問題：
如何使當前所在頁面的Link變成粗體？

實作想法：
![image](https://hackmd.io/_uploads/BJuQHLw9a.png)

實作程式碼：
**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的函式庫，可以直接看他們各自的官方文件。
![2024-01-31 13-14-33 的螢幕擷圖](https://hackmd.io/_uploads/rkVcP8P9T.png)
