# Full React Tutorial part2
###### tags: `Javascript, React`
# Making a Custom Hook
我們在之前的篇章中看到了各種的 state 的使用,不管是 blogs fetch 到的資料、資料 loading 時候使用的 state 或是錯誤的訊息更新到頁面的 state ,但是如果我們其他 component 也要使用的話這些 state 就得全部重新寫一遍?
當然不用
所以最好的方法就是把這些東西放到 component 內,就可以重複使用瞜!
然而當我們把這樣的 邏輯寫法把它整個拉出去寫另外的 component 的這個方法會需要使用到 **Custom Hook** 這個概念
## 首先我們建立一個 useFetch.js 檔案
1. 引入我們要用的資料
`import { useState, useEffect } from 'react';`
並且建立元件然而注意這邊因為是 Custom Hook 的關係 function 的命名必須符合一個標準
function 名稱 必須是`use+...`
2. 下一步必須 `export default useFetch`
所以目前函式會長這樣
```javascript=
import { useState, useEffect } from 'react';
const useFetch = (url) => {
}
export default useFetch;
```
## 把剛剛做好的 useEffect 整個拉進來
記得拉的時候要連帶 state 一起拉進來才可以更新頁面,接下來需要修改一些地方
1. 這個 useFetch 會需要從 Home.js 帶入參數也就是網址的部分
2. 並且使用在內部的 fetch 方法
3. 修改 blogs 名稱改為 data 為了因應每一篇的資料內容不一樣使用比較廣泛的名詞代替(setBlogs 改為 setData)
4. 最後函式必須返回三個東西,也就是 data, insPending, error 回去 Home.js 做使用
```javascript=
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => {
if (!res.ok) {
throw Error('could not fetch the data from that resource');
}
return res.json()
}).then((data) => {
setData(data);
setIsPending(false);
setError(null);
}).catch(err => {
setError(err.message);
setIsPending(false);
console.log(err.message);
})
}, [url])
return { data, isPending, error }
}
export default useFetch;
```
## 回到 Home.js 頁面
畫面精簡不少
1. import 處少了 useState, useEffect 因為用不到了
2. 整個 state, useEffect 的部分全部拆出去
3. 引入 useFetch 使用
4. 使用解構式 直接提取 data, isPending, error 這三個被返回的值
5. 並且填入 JSX 中
6. 注意 blogs 有改名變成 data
```javascript=
import BlogList from './BlogList';
import useFetch from './useFetch';
const Home = () => {
const title = "I am title"
const { data, isPending, error } = useFetch('http://localhost:8000/blogs')
return (
<div className="home">
{error && <div>{error}</div>}
{isPending && <div>Loading...</div>}
{data && <BlogList blogs={data} title={title} />}
</div >
);
}
export default Home;
```
# The React Router
通常你的網頁專案不會只包含一個頁面,通常會有很多的頁面,而在 React 中會透過 React Router 去到不同的頁面
## 沒有使用 react 的網頁
會針對每個使用者行為回傳對應的 html 頁面,比方說他按了 contact 頁面,server 就會回傳 contact 頁面的 html 回去,如此不斷重複

## React 網頁跟非 React 網頁
react 則會把上述那些行為在瀏覽器直接解決
但一開始還是會有使用者發出 request

接下來 server 會回傳 react js bundle 回來,從這個時刻開始 react 就可以掌管整個網頁

這時候 react 就會動態地把資料呈現在頁面上

像是剛剛的例子 使用者點了 contact 頁面,這時候由於 react 接手了,所以就不需要把 request 傳回 sever 而是由 react 直接回傳 DOM 動態的更新頁面

因此不管使用者點 home, contact, about 都會直接由 react 處理更新頁面,不會再回傳到 server ,因此整個網頁感覺速度會提升並且更滑順

## Set it up
首先必須先下載 React router 插件進去專案使用,這邊使用版本 5 (當下穩定版)
` npm i react-router-dom@5`
引入 router 進入專案大括號內是會用到的元件
`import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';`
下一步到 App.js 也就是 root component 內部 Router tag 使用並且包住所有的程式碼
接下來可以看到幾個部分:
1. Navbar 並沒有被包在 Switch 內因為 Switch 內部的 component 只會顯示其中一個,其他的會被關閉(Navbar 需要常駐在頁面上)
2. Switch 內部添加了 Route tag 並且寫上 path = "/" 通常這邊的指向是 home page
```javascript=
import Navbar from './Navbar'
import Home from './Home'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
function App() {
return (
<Router>
<div className="App">
<Navbar />
<div className="content">
<Switch>
<Route Path="/">
<Home />
</Route>
</Switch>
</div>
</div>
</Router>
);
}
export default App;
```
# Exact Match Routes
這邊為了可以展示新的 Routes 所以要再製作一個新的頁面用來新增 blogs
首先我們製作新的 component
並引入 App.js
```javascript=
const Create = () => {
return (
<div className="create">
<h2>Add a New Blog</h2>
</div>
);
}
export default Create;
```
內部的寫法就跟之前的 Home 一樣
```javascript=
import Create from './Create';
function App() {
return (
<Router>
<div className="App">
<Navbar />
<div className="content">
<Switch>
<Route path="/">
<Home />
</Route>
<Route path="/create">
<Create />
</Route>
</Switch>
</div>
</div>
</Router>
);
}
```
可是當我們這時候更新頁面卻發現,輸入網址 /create 沒有任何變化還是顯示首頁是因為
* /
* /create
如果沒有寫上屬性 exact 的話會被辨別過關,也就是明明路徑是 /create ,但是 / 就已經被辨別過關所以直接顯示了 Home,因此這邊就要使用 exact 這個屬性讓路徑的顯示變成絕對的就不會出現這個問題摟

## Switch
Switch 這個 tag 會確保在其中 route 只會同時顯示一個在頁面上,所以當使用者的 request 傳到 react 並且跟 route 有關時,Switch 會上到下的檢視程式碼並且去符合其要求
換句話說如果沒有 Switch 包住這些 route ,React 會直接把裡面的 component 全部渲染到頁面上(像是 Navbar 一樣)
上面有提到 request 是傳到 react 後經過處理再回傳畫面,但是目前我們這樣設定 path 的方式依舊得回傳 server 要求資料,所以在下個章節會針對這裡說明
# Router Links
為了做到由 react 來攔截使用者的 request 則必須使用到特殊的 tag 也就是 Router Link
* 首先必須引入這個特殊的 tag
* 替換原本的 a tag 換成 Link tag
* 後面的 herf 替換成 to
這樣就可以攔截 request 到 react router 本身摟
```javascript=
import { Link } from 'react-router-dom';
const Navbar = () => {
return (
<nav className="navbar">
<h1>The Dojo Blog</h1>
<div className="links">
<Link to="/">Home</Link>
<Link to="/create" >New Blog</Link>
</div>
</nav>
);
}
export default Navbar;
```
接下來要展示的是如果當你切換頁面,但是上一頁的資料卻還在載入中,你沒等他跑完就換頁了這時候會抱錯,因為 react 會想把剛剛載入的資料呈現到頁面上可以頁面卻切換了

會在下個章節解釋該如何處理
# useEffect Cleanup
這邊要實作一個功能來停止 fetch 當我們已經換頁不需要上一頁的內容時,這邊就可以使用一個 clean up function 在 useEffect Hook 內的 abort controller 他是一個 JS 的 API ,當畫面要渲染到頁面上時就會被觸發
首先我們到 useFetch.js 檔案操作
1. 為了使用 abort controller ,這邊的使用方式是用變數先做指派,這邊就可以跟 fetch request 的方法產生連結
`const abortCont = new AbortController();`
2. 接下來在 fetch 第二個參數輸入:
`{ signal: abortCont.signal }`
3. return abort 內容(位置放在 return 這邊是因為它會在頁面切換後返回內容這邊可以用 console.log 測試)
2. 在處理完 abort 後,為了不再顯示錯誤訊息所以在 err 的部分做判斷式,如果 error 出現的內容是 AbortError 也就是我們處理的 clean up function 這時就顯示我們要的字串代替錯誤訊息
```javascript=
useEffect(() => {
const abortCont = new AbortController();
setTimeout(() => {
fetch(url, { signal: abortCont.signal })
.then(res => {
if (!res.ok) { // error coming back from server
throw Error('could not fetch the data for that resource');
}
return res.json();
})
.then(data => {
setIsPending(false);
setData(data);
setError(null);
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('fetch aborted');
} else {
setIsPending(false);
setError(err.message);
}
})
}, 1000);
return () => abortCont.abort();
}, [url])
return { data, isPending, error };
}
```
# Route Parameters

不同的 route 參數可以帶我們到不同的內容頁面上,這個章節要示範如何設置 router parameters
## 首先我們得設置一個新的 component
BlogDetails 來呈現每一篇 blog
1. 為了可以使用 route 的參數必須使用 hook useParams
`import { useParams } from "react-router-dom";`
2. 引入方式也跟 useState 很像使用設置變數的方式來引入 App.js 的變數
3. 動態的使用在 JSX 上面
`const { id } = useParams();`
```javascript=
import { useParams } from "react-router-dom";
const BlogDetails = () => {
const { id } = useParams();
return (
<div className="blog-details">
<h2>Blog details - {id}</h2>
</div>
);
}
export default BlogDetails;
```
## 在 App.js 引入 Route 內部
1. 引入 component
1. 操作方式要在 path 的地方加入 `/:後面自取變數名稱`
```javascript=
import BlogDetails from './BlogDetails';
function App() {
return (
<Router>
<div className="App">
<Navbar />
<div className="content">
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/create">
<Create />
</Route>
<Route path="/blogs/:id">
<BlogDetails />
</Route>
</Switch>
</div>
</div>
</Router>
);
}
```
## BlogList 處理點擊後跳轉到 BlogDetails
一樣要 import Link 進來使用 request 才不會回傳到 server
還記得我們 json 資料其實還有個 id 還沒有使用到
這邊我們在 JSX 中操作動態的輸入變數進去
使用 `${內部就可以放入變數操作摟}`
`<Link to={`/blogs/${blog.id}`}>`
```javascript=
import { Link } from "react-router-dom";
const BlogList = ({ blogs, title }) => {
return (
<div className="blog-list">
<h2>{title}</h2>
{
blogs.map((blog) => (
<div className="blog-preview" key={blog.id}>
<Link to={`/blogs/${blog.id}`}>
<h2>{blog.title}</h2>
<p>Written By {blog.author}</p>
</Link>
</div>
))
}
</div>
);
}
```
這樣就可以達到點每個 blog 會到達每個 blog detail 的頁面摟 !
# Reusing Custom Hooks
這篇主要繼續來操作 useFetch 這個 component ,示範如何重複使用這個它
因為我們要在 BlogDetails.js 內部 取得 json-server 的資料(blog)、 isPending、 error message 所以在這邊引入 useFetch component
`import useFetch from './useFetch';`
1. 接下來使用解構式把需要用到的 data, error, isPending 從 useFetch component 中取出,fetch 網址的部分因為要抓單篇所以要串接上 id
1. 這邊的 id 因為已經在 useParams 取得了所以直接連接到網址上面
2. 接下來一樣使用 && 條件式來判斷只有當左邊值為 true 時右側才會執行
3. 一一填入 JSX
```javascript=
const BlogDetails = () => {
const { id } = useParams();
const { data: blog, error, isPending } = useFetch('http://localhost:8000/blogs/' + id);
return (
<div className="blog-details">
{isPending && <div>Loading...</div>}
{error && <div>{error}</div>}
{blog && (
<article>
<h2>{blog.title}</h2>
<p>Written by {blog.author}</p>
<div>{blog.body}</div>
</article>
)}
</div>
);
}
```
正常印出的內容如下

# Controlled Inputs (forms)

會在此處產出 form 的表單格式並且使用 state 追蹤所有的 input change 並且更新在下方
## 這篇主要會處理 form 表單在 Create.js component 內操作
1. 首先得使用 useState 引入 component
1. 指派 useState 給變數
2. 在 form 內部 value 都填入對應的值
3. 並且為了呈現回去 form 內部因此使用 onChange 事件傳回去 form 作呈現
4. 最後為了看到輸入的值,因此在頁面使用大括號呈現上面 form 抓到的 value
```javascript=
import { useState } from 'react';
const Create = () => {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [author, setAuthor] = useState('mario');
return (
<div className="create">
<h2>Add a New Blog</h2>
<form>
<label>Blog title:</label>
<input type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)} />
<label>Blog body:</label>
<textarea
required
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<label>Blog author:</label>
<select
value={author}
onChange={(e) => setAuthor(e.target.value)}
>
<option value="mario">mario</option>
<option value="yoshi">yoshi</option>
</select>
<button>Add Blog</button>
<p>{title}</p>
<p>{body}</p>
<p>{author}</p>
</form>
</div>
);
}
```
這個 form 的最終目的還是要把填寫 blog 推到 server,會在下一篇中介紹 Submit 事件
# Submit Events
```javascript=
<form onSubmit={handleSubmit}>
// 操作的程式碼 ....
</form>
```
```javascript=
const handleSubmit = (e) => {
e.preventDefault();
const blog = { title, body, author };
console.log(blog)
}
```
1. 這邊操作的 Submit 事件會放在整個 form 的屬性並且會觸發一個函式
1. 設置函式內容為
* preventDefault 會避免預設行為刷新頁面
* 使用 解構 抓取 title, body, author form 內部的內容
* 最後我們印出來看看有沒有抓取成功
可以看下方紅框是有正確輸出的

下一個步驟會針對做出來的這個 blog 物件使用 POST 方法推到 server 上面
# Making a POST Request
這篇會把 Create.js 產生出來的 blog 藉由 button 點擊後觸發的 Submit 事件送進去 json-server 裡面
對 Submit 事件的函式新增 fetch 函式
1. 使用 POST 方法
1. header 的部分操作 "Content-Type": "application/json" 代表使用的是 json 資料格式
1. body 的部分因為傳進 json-server 所以要先把 blog 轉換成 json
2. 在 .then 的部分等到資料都傳輸完成後則執行 setIsPending 為 false 也就是沒有再等待傳輸了已經傳輸成功
```javascript=
const handleSubmit = (e) => {
e.preventDefault();
const blog = { title, body, author };
setIsPending(true);
fetch('http://localhost:8000/blogs/', {
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify(blog)
}).then(() => {
console.log('new blog added');
setIsPending(false)
})
}
```
## 操作 Loading 時的按鈕字樣
這邊作者操作的只有按鈕的字樣部分做 Loading...
* 首先指派變數
` const [isPending, setIsPending] = useState(false);`
* 接下來操作 DOM
這邊的邏輯是
1. 如果 isPending 是 true 則正常顯示 Add Blog
2. 如果 isPending 是 false 則顯示按鈕不能按 disabled 並且內部字樣改變加入 ...
```javascript=
{!isPending && <button >Add Blog</button>}
{isPending && <button disabled>Add Blog...</button>}
```
這樣就成功 POST 到 json-server 搂

# Programmatic Redirects
當我們送出新的 Blog 的瞬間通常頁面可以能會跳轉到其他地方比方說首頁或是可以呈現 Blog 的地方,這樣轉址的方式該怎麼做呢 ?
## 使用新的 React hook useHistory
引入 useHistory 他是 react-router-dom 的插件
`import { useHistory } from 'react-router-dom';`
指派給變數以便呼叫使用
`const history = useHistory('');`
操作時機在於加完成 blog 之後
使用 `history.push('內部填入要轉往的網址')` 就可以達到轉址的功能瞜
我有註解到一個 go 的方法,他可以實踐上一頁或是下一頁的功能
```javascript=
fetch('http://localhost:8000/blogs/', {
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify(blog)
}).then(() => {
console.log('new blog added');
setIsPending(false);
// history.go(-1);
history.push('/');
})
```
# Deleting Blogs
為了要刪除 blogs 必須使用 DELETE methods
進入 BlogDetails.js
在 JSX 的部分添加 button 並且設置點擊事件
`<button onClick={handleClick}>delete</button>`
handleClick 函式
* 在使用一個 fetch 來刪除 json-server 內部的資料以便刪除 blog 內容
* fetch 參數的部分一樣使用 json-server 的網址在串接上 id 就可以確認要刪除哪一篇
* method 的部分簡單的使用 DELETE
* 最後在刪除完成之後,使用轉址回去 home page
```javascript=
const handleClick = () => {
fetch('http://localhost:8000/blogs/' + blog.id, {
method: 'DELETE'
}).then(() => {
history.push('/');
})
}
```
# 404 Pages & Next Steps
## 錯誤訊息的處理
設置一個新的 component NotFound.js
1. 引入 Link
1. 填入 JSX 內容文字
1. 再返回 homepage 字樣 使用 Link tag
```javascript=
import { Link } from "react-router-dom"
const NotFound = () => {
return (
<div className="not-found">
<h2>Sorry</h2>
<p>That page cannot be found</p>
<Link to="/"> Back to the homepage...</Link>
</div>
);
}
export default NotFound;
```
## 把這個 Route 引入 App.js
* 首先引入要使用的 component NotFound
* 加入 Route 到最下層
* path 的部分是重點 使用 `"*"` 代表全部的意思,所以必須放在最下層,如此一來只要網址不符合上面 path 的部分就會全部引入這個 Route
```javascript=
import NotFound from './NotFound';
function App() {
return (
<Router>
<div className="App">
<Navbar />
<div className="content">
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/create">
<Create />
</Route>
<Route path="/blogs/:id">
<BlogDetails />
</Route>
<Route path="*">
<NotFound />
</Route>
</Switch>
</div>
</div>
</Router>
);
}
```
正常印出功能就做完搂 !
