React 學習筆記(3) - Router === ![](https://i.imgur.com/ZLirEbH.png) ###### 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) ![](https://i.imgur.com/P5KqzU2.png) - 打開 VScode 及專案資料夾 - 在 `src` 資料夾內建立一個 `pages` 的資料夾,以及三個分頁 `Home.js`, `About.js`, `Contact.js` ![](https://i.imgur.com/rq1VBgw.png) - 開啟 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 ![](https://i.imgur.com/EDGhwwG.png) ### 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 了? ![](https://i.imgur.com/ZAJEENw.png) - 主要因為即使我們已經引入 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> ``` ![](https://i.imgur.com/aTfO9r1.png) ### 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 會重新抓取資料 ![](https://i.imgur.com/LGsgRjy.png) ![](https://i.imgur.com/l1lxsXn.png) - 使用 `<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` ![](https://i.imgur.com/eOjMjxm.png) ```javascript= // App.js <nav> <h1>My Articles</h1> <NavLink exact to='/' >Home</NavLink> <NavLink to='/about' >About</NavLink> <NavLink to='/contact' >Contact</NavLink> </nav> ``` ![](https://i.imgur.com/1g5pDQ1.png) --- ### 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 範例 ![](https://i.imgur.com/rptzDm4.png) - 首頁呈現畫面 ![](https://i.imgur.com/qGeUnLQ.png) --- ### 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 的文章分頁 ![](https://i.imgur.com/dzyR9lb.png) --- ### 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 也會將這段參數顯示出來 ![](https://i.imgur.com/tSUhnfl.png) ```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> ); } ``` - 可以把參數動態地渲染到畫面上 ![](https://i.imgur.com/Ji6dVZw.png) --- ### 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> ```