# 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)