# React(MRWR)第 6 節: How to Handle Forms
> Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/)
`20230818Fri.~20230825Fri.`
:::danger
6-85 在哪個檔案中定義state?
6-90 陣列在React中如何更新state?
:::
## 6-83. App Overview
這個章節要實作一個閱讀清單(如下圖),他的功能包含:
1. 可以在下方自行輸入內容,按下submit之後,在閱讀清單中新增卡片
2. 可以按下卡片右上角的"X",刪除該卡片
3. 可以按下卡片右上角的鉛筆icon,編輯該卡片的名稱

****
## 6-84. Initial Setup
一開始利用`npx create-react-app 06-books`建立專案,接著同之前一樣,把`src`資料夾中的所有檔案給刪除(這是為了確保我們的程式碼都能跟老師一樣)。
然後在`src`中建立`index.js`
**index.js**
```javascript!
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.js";
const el = document.getElementById("root");
const root = ReactDOM.createRoot(el);
root.render(<App />);
```
再來回頭看看這次專案的整個元件,以及元件之間的階層關係


同樣的,我們在這裡也會新增一個`components`資料夾來存放所有元件檔案,以便更好整理。
****
## 6-85. State Location
再來我們可能會思考,我們要如何取得與儲存使用者在input框框裡輸入的內容呢?又如何將他們顯示到上方的清單?以及如何可以讓使用者每一次都可以對卡片做編輯的動作?
答案是我們要利用「物件(object)」。

我們將資料用物件的方式呈現,再把物件轉變成卡片。並且把這些物件通通放進同一個陣列(array)中,如此一來,我們就擁有一個資料結構,存放所有使用者輸入的資料了。
當然,每當使用者想去「更新」這些書籍資料(無論是新增、編輯、刪除),只要在React中談到「更新」,毋庸置疑的肯定跟"state"脫離不了關係的!
不過問題是,我們該在哪裡定義「書籍的state」?
老師這裡提供了一些想法:

首先,我們必須知道在rerender的時候,他會重新渲染state被定義的所在之處,以及該處的子元件,都會被重新渲染,所以我們可以先找出所有會用到這個state的component全部抓出來,接著從中找出最適合定義state的component,也就是最接近這些需要state的component的父元件。
而在此專案中,我們可以看見,除了`App.js`外,其他檔案皆會用到書籍的state。

所以我們可以將書籍的state放置在`app.js`之上

因此,我們在`App.js`檔案中新增state system。並且設置初始值為一個空陣列(`[]`)。
**App.js**
```javascript!
import { useState } from "react";
function App(){
const [books, setBooks] = useState([]);
return(
<div>App</div>
)
}
export default App;
```
****
## 6-86. Reminder on Event Handlers
* 第一步驟:輸入內容至input框框

* 第二步驟:按下submit之後,books的state會從原先初始值空陣列,更新為新增了一筆書籍資料(包含id)。

* 第三步驟:此時,按下submit的同時,除了更新books的state之外,還會同時新增一張卡片到螢幕上。

而要對於books的state動作,我們這裡建立三個function。

****
## 6-88. Receiving New Titles
這個章節將要來接收使用者輸入在input框框中的值,這裡會使用到前面提到的function:"createBook",而這個function的位置建立在`App.js`,這在6-85中有提到「state的位置該放在哪裡?」,以此專案為例,擺在`App.js`最為適當。
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">App.js</h3>
</div>
1. 那我們先來建立createBook這個function,這個function會接收一個參數叫做title,亦即用來放使用者輸入的title。(不過function執行的內容先用console.log做測試)
2. 引入BookCreate元件,並建立props叫做"onCreate",該值為createBook這個function。
**App.js**
```javascript!
import { useState } from "react";
import BookCreate from "./components/BookCreate"
function App(){
const [books, setBooks] = useState([]);
const createBook = () => {
console.log("Need to add book with: ", title);
}
return(
<div>
<BookCreate onCreate={createBook}/>
</div>
)
}
export default App;
```
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">BookCreate.js</h3>
</div>
1. 接著來看`BookCreate.js`中的內容。先看看我們需要加什麼在這個元件之中(如下圖),包含了label、input以及一個button。

2. 接著每當提到input,就要想到input的attribute: value跟onChange,這樣我們才能不斷地更新使用者在input框框內輸入的內容。
其中:
* value={title}對應到
```javascript!
const [title, setTitle] = useState("")
```
* onChange={handleChange}對應到
```javascript!
const handleChange (event) => {
setTitle(event.target.value)
}
```
3. 我們希望使用者在input框框輸入之後,無論按下enter或是按下button提交這個form的資料時,使用者輸入的內容仍然可以被保留著,因此這裡需要用到`event.preventDefault()`,除了使用者輸入的內容保留之外,還要更新books的state,因此會動用到prop中的onCreate function(從父元件傳遞過來的) ,並且把title作為參數傳入onCreate()中。
並且在submit之後,讓input框框理清空。
我們把這些包成一個finction:
```javascript!
function handleSubmit = (event) => {
event.preventDefault();
onCreate(title);
setTitle("");
}
```
**BookCreate.js**
```javascript!
import { useState } from "react";
function BookCreate({onCreate}){
const [title, setTitle] = useState("");
const hadleChange = (event) => {
setTitle(event.target.value)
}
const handleSubmit = (event) => {
event.preventDefault();
onCreate(title);
setTitle("");
}
return(
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="title">Title</label>
<input value={title} onChange={hadleChange} id="title" />
<button>click</button>
</form>
</div>
)
}
export default BookCreate;
```
****
## 6-90. Updating State
> 參考資料:[深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/)
接著這個章節要來更新books state,books是一個陣列,所以我們可能會聯想到,用陣列中的method "push"來新增新的書籍(如下圖的方法),但我們會發現這完全不起作用!

為什麼呢?若前後兩次的參考都指向同一個陣列或物件,React會認為不需要rerender(重新渲染),因此螢幕上就不會有任何的變化。

如果要解決這個問題,就得利用展開運算子,展開運算子會有深拷貝的效果(僅限於一層的物件,可以參考[聊聊 JavaScript 中的深拷貝 (上)](https://blog.errorbaker.tw/posts/clay/copy/)以及Huli寫得[深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/))。
PS 這裡運用到展開運算子的概念,而運用展開運算子的原因又牽扯到JS中的深拷貝。
所以,我們先來看看JS在這裡做了哪些事情?可以看出與原先利用push新增陣列內容不同,這次是直接創建新的陣列!

而此時的React(即`setBooks(books)`的部份)就會發現之前的參考與現在的參考位址不同了耶,於是React就會去做rerender(重新渲染)的動作了。

****
## 6-91. Don't Mutate That State!
所以之後在使用useState的時候,可以先想想看state(在底下範例為something)是不是陣列或物件呢?如果是的話,就必須注意要非常小心去更新這個state!

:::warning
不要直接去轉變、改變、修改陣列或是物件(當陣列或物件作為State的時候)
Do not mutate/change/modify arrays or objects.(when they are used as state)
:::
老師提供了他自製的「[關於state的更新](https://state-updates.vercel.app/)」的清單。
## 6-93~103. [Optional] 關於如何更新陣列、物件的State
接下來的第93到版小節,都是選擇性的章節,關於如何更新陣列、物件的State,所以把這些章節的內容放在一起了,應該會比較好找到要找的內容。
:::danger
讀到這邊有點困惑,所以稍微查了下
> 參考資料:[Does array.filter() create a new array?](https://stackoverflow.com/questions/63941282/does-array-filter-create-a-new-array)
裡面有提到:「.filter creates a new array, but the new array is the only new structure that is created. The items inside the array remain unchanged.」
也就是`.filter`的確會建立一個新的陣列,但是陣列中若是物件(即Object data,非Primitive type data),則會直接複製原先舊陣列中的資料,也就是所謂的淺拷貝。
:::
### █ 6-93. Adding Elements to the Start or End
新增新的元素到原先陣列or物件的最後或最前面,會運用到「展開運算子」、「其餘運算子」。
**◈ 新增到前面**
其餘運算子

**◈ 新增到後面**
展開運算子

### █ 6-95. Inserting Elements
那如果我想新增元素放在原先陣列or物件的中間呢?(亦即「插入」),除了用到「展開運算子」、「其餘運算子」,還需要"slice()"函式以及設置變數"index"來幫助!
**◈ slice()**
slice是array method其中之一,可以參考MDN的文章[Array.prototype.slice()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)。
```!
slice() 方法會回傳一個新陣列物件,為原陣列選擇之 begin 至 end(不含 end)部分的淺拷貝(shallow copy)。而原本的陣列將不會被修改。
```
slice的語法:
```javascript!
arr.slice([begin[, end]])
```
slice的舉例:


### █ 6-97. Removing Elements
那如果想要移除其中的元素呢?這就需要利用array method中的`filter()`來解決了!`filter()`是一個幫忙過濾資料的好工具。
**◈ filter()**
可以參考MDN上面的文章[Array.prototype.filter()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)。
```!
filter() 方法會建立一個經指定之函式運算後,由原陣列中通過該函式檢驗之元素所構成的新陣列。
```
filter的語法:
```javascript!
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
```
可參考阿傑的array method[Day 7 咩色用得好 - Array.prototype.filter](https://ithelp.ithome.com.tw/articles/10296254)
只有return值為true時,filter()才會把值加入新陣列。

slice的舉例:



### █ 6-99. Modifying Elements
接著是修改陣列中的元素。

### █ 6-102. Adding, Changing, or Removing Object Properties
**增加或改變物件中的屬性**

**從物件中移除屬性**

這個作法其實就是利用其餘運算子與解構賦值的概念,以下方為例:
```javascript=!
const [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]
```
再看回課程中的舉例,把他寫成跟上方例子類似會更好懂:
```javascript=!
const {color, ...rest} = {color: 'red', name: 'apple'};
console.log(color); //red
console.log(rest); //{name: 'apple'}
```
所以說若我們想移除color這個屬性,就可以用上述作法,最後提取rest變數時,rest裡頭就不再有color屬性了。
老師的圖例說明:

****
## 6-105. Generating Random ID's
一般來說ID會由後端處理,不過這裡沒有後端,所以暫且用隨機數字來取代,先使用`Math.random()`乘上999,使之產生0~999之間的數字,之後在使用`Math.round()`取四捨五入。
**App.js**

****
## 6-106. Displaying the List
這個章節要將清單顯示到螢幕上。

我們在前面提到把books state放在`App.js`上,之後傳入`BookList.js`裡面,再將單本book的資料傳給`BookShow.js`。
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">App.js</h3>
</div>
所以首先要在`App.js`放入BookList的元件,並且我們要從`App.js`傳送Books props給`BookList.js`
**App.js**
```javascript=!
import { useState } from "react";
import BookCreate from "./components/BookCreate";
import BookList from "./components/BookList";
function App(){
const [books, setBooks] = useState([]);
const createBook = (title) => {
const updatedBooks = [
...books,
{
id: Math.round(Math.random()*999),
title
}
]
setBooks(updatedBooks);
}
return(
<div className="app">
<BookList books={books}/>
<BookCreate onCreate={createBook}/>
</div>
)
}
export default App;
```
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">BookList.js</h3>
</div>
1. 接著看到`BookList.js`,我們要將「從`App.js`傳入到`BookList.js`」的books prop給map到元件裡面,讓books這些資料可以顯示到畫面上。(所以要記得function BookList的參數要把prop給代入)
2. 而books運用map(),每一本都轉變成一個BookShow元件
3. BookShow元件上建立兩個props,分別為key prop以及book prop
**BookList.js**
```javascript!
import BookShow from "./BookShow";
function BookList({books}){
const renderedBooks = books.map((book) => {
return <BookShow key={book.id} book={book}/>;
})
return(
<div className="book-list">{renderedBooks}</div>
)
}
export default BookList;
```
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">BookShow.js</h3>
</div>
最後來到`BookShow.js`,我們剛才已經在`BookShow.js`的父層`BookList.js`中創建兩個props,而其中之一:book為我們所需,所以這裡要將prop傳入到BookShow元件!
**BookShow.js**
```javascript!
function BookShow({book}){
return(
<div className="book-show">{book.title}</div>
)
}
export default BookShow;
```
****
## 6-107. Deleting Records
這裡將要新增一個X在右上角,讓我們可以去刪除原先建立的清單內容。而我們要刪除,就需要更新,需要更新在React中就是要用到state。
這是我們希望的流程:

實做上,我們需要建立一個function叫做`deleteBookById()`

首先,建立function `deleteBookById()`,並設定參數`id`,我們預期之後會傳入使用者想刪除的書籍之id。
**App.js**
```javascript!
const [books, setBooks] = useState([]);
const deleteBookById = (id) => {
函式內容
}
```
接著完成函式內容。我們需要跑一遍整個books state陣列中目前的所有物件(也就是單本book物件),所以會使用array method,而這裡我們要找出與id相符的book,並且刪除它,因此這裡將選擇的array method即為`filter()`!(前面說過`filter()`就是一個篩選資料的好工具)
其中要特別注意的就是,filter裡頭只會return結果為true的book,以此處為例,即只有該book的id不等於傳入的id,才會return該book。相反的,若該book的id等於傳入的id的話,那本書就是要刪除的書!
**App.js**
```javascript!
const deleteBookById = (id) => {
const updatedBooks = books.filter((book) => {
return book.id !== id;
});
}
```
最後,當然就是要把這個state更新。
```javascript!
const deleteBookById = (id) => {
const updatedBooks = books.filter((book) => {
return book.id !== id;
});
setBooks(updatedBooks);
}
```
再來,要思考如何把這個function `deleteBookById()`從`App.js`往底下的元件傳呢?答案是利用"props system"。
在`App.js`中的`<BookList />`元件上加上prop叫做"onDelet",然後把function `deleteBookById()`賦予"onDelet"這個prop。
**App.js**
```javascript!
return(
<div className="app">
<BookList books={books} onDelet={deleteBookById}/>
<BookCreate onCreate={createBook}/>
</div>
)
```
然後還要將function `deleteBookById()`再從`BookList.js`繼續往子元件`BookShow.js`傳,所以一樣使用props system,將該function傳到最底層元件`BookShow`。
所以來到`BookList.js`檔案中,記得先接收onDelete prop,要把onDelete prop作為參數,傳入BookList()。
並且在`BookList.js`檔案裡面找到`<BookShow />`,一樣先建立prop(這裡皆命名"onDelete")。
**BookList.js**
```javascript!
import BookShow from "./BookShow";
function BookList({books, onDelete}){
const renderedBooks = books.map((book) => {
return <BookShow onDelete={onDelete} key={book.id} book={book}/>;
})
return(
<div className="book-list">{renderedBooks}</div>
)
}
export default BookList;
```
再來,就是把prop作為參數傳給`BookShow()`。並且我們要促發這個onDelete,所以還要建立一個button,等著使用者去按下,當使用者按下這個button,就會觸發onClick事件,不過onClick事件觸發後我們並不想要直接執行onDelete,因為我們需要傳入id,我們的程式才會知道要刪除掉哪筆資料。
所以onClick被觸發之後,我們讓他去執行另一個funciton,叫做handleClick。
而handleClick裡面執行onDelete(),並傳入引數book.id到onDelete()。
**BookShow.js**
```javascript!
function BookShow({book, onDelete}){
const handleClick = () => {
onDelete(book.id);
}
return(
<div className="book-show">
{book.title}
<div className="actions">
<button className="delete" onClick={handleClick}>
Delete
</button>
</div>
</div>
)
}
export default BookShow;
```

****
## 6-108. Toggling Form Display
再來我們要新增一項新功能,讓使用者可以去編輯title,所以我們要再做一個元件,叫做BookEdit。

而畫面上的book卡片,則會有兩種狀態,這兩眾狀態由不同元件組成,不是「顯示book title」,就是「顯示book edit的元件樣式」。

每當使用者點擊鉛筆的icon就會進入編輯模式,而當螢幕上的內容會改變,勢必就是會牽扯到state的概念!

但這個state要放在哪裡呢?可以回顧[6-85](https://hackmd.io/1yrGu38OQEanXXK56rAO8A?view#6-85-State-Location)的作法,這個時候編輯與否的state僅會用於`BookEdit.js`中,因此我們要將這個state定義在離他最近的父元件上,在這裡也就是`BookShow.js`。
這個state命名為showEdit,而這個state變數讓他的type為boolean,因為我們只須知道現在為編輯與否。
當showEdit為true,顯示BookEdit元件;相反的,當showEdit為false,則顯示book的title。

所以先來到`BookShow.js`,先來定義state,要使用useState,務必記得要import useState進入檔案。
```javascript!
import { useState } from "react";
```
接著開始設定useState,並且我們將showEdit的初始值設為false,因為我們不希望一開始就進入編輯模式:
```javascript!
const [showEdit, setShowEdit] = useState(false);
```
另外可以注意的點,當我們點擊鉛筆icon時,會觸發onClick去執行handleEditClick函式。而handleEditClick函式會將state改成"!showEdit"並用`setShowEdit()`存下來。
(!showEdit亦即與目前狀態的相反,假如目前是false,則點擊後會執行"!showEdit",也就變成了true。)
**BookShow.js**
```javascript!
import { useState } from "react";
function BookShow({book, onDelete}){
const [showEdit, setShowEdit] = useState(false);
const handleDeleteClick = () => {
onDelete(book.id);
};
const handleEditClick = () => {
setShowEdit(!showEdit);
};
return(
<div className="book-show">
{book.title}
<div className="actions">
<button className="edit" onClick={handleEditClick}>
Edit
</button>
<button className="delete" onClick={handleDeleteClick}>
Delete
</button>
</div>
</div>
)
}
export default BookShow;
```
再來,就是要在原先book title的地方做點改變,到底要顯示book title呢?還是要顯示bookEdit元件呢?
首先,依然還在`BookShow.js`檔案之中,然後先將BookEdit作為元件傳入該檔案裡。
```javascript!
import BookEdit from "./BookEdit";
```
然後我們要利用一些邏輯語法來達成我們的目標(決定到底要顯示title還是BookEdit元件)
而前面剛提過,決定到底要顯示什麼,是由ShowEdit 這個state來決定的(當showEdit為true,顯示BookEdit元件;相反的,當showEdit為false,則顯示book的title。)
```javascript!
let content = <h3>{book.title}</h3>;
if(showEdit){
content = <BookEdit />;
}
```
****
## 6-109. Default Form Values
父元件差不多佈置好整個state了,接下來要來修改BookEdit的元件內容,先把大致的結構建立起來。
```javascript!
function BookEdit(){
return(
<form className="book-edit">
<label>Title</label>
<input className="input" />
<button className="button is-primary">
Save
</button>
</form>
)
}
export default BookEdit;
```
接著,我們要針對input,因為使用者進入到編輯模式,可能會在input重新輸入內容,因此我們要設置state來處理input裡的內容。
另外還有設置handleSubmit,來處理使用者按下save按鈕後提交表單所觸發的函式。
:::danger
要submit表單時,都需要注意,要記得把form的default先清除掉。
```javascript!
event.preventDefault();
```
:::
接著要讓input裡面有預設內容,預設內容即原先的title。
這裡就要回到BookShow裡面了,我們要在`BookShow.js`裡的BookEdit元件建立props object,也就是把book繼續往子元件傳下去。

****
## 6-110. Updating the Title
我們已經大致弄好BookEdit的架構了,問題是我們編輯完title之後,要如何更新books的state呢?因此這裡的作法就跟前面要刪除book一樣,我們需要在`App.js`裡建立function,然後將這個function透過props system往下傳至`BookEdit.js`中。
因此我們在`App.js`中建立function `editBoolById(id, title)`

透過`onEdit` prop將function `editBoolById(id, title)`往下傳(即便BookList跟BookShow用不到,但他們兩是到達BookEdit的必經之路,所以仍然要傳入prop)

那我們先來到`App.js`創建ㄏ,我們要把整個books去一筆筆對照id,看看是否跟使用者想修改的book的id相符合,如果是的話,我們就要把原先的title改成newTitle,而newTitle即使用者輸入的新title內容。
至於如何一筆筆對照呢?利用array method中的map()
至於如何只修改符合id的book呢?利用if判斷式
至於如何修改book的內容呢?利用展開運算子,並修改想修改的title
至於如何更新資料呢?利用state去更新
```javascript!
const editBookById = (id, newTitle) => {
const updatedBooks = books.map((book) => {
if(book.id === id){
return {...book, title: newTitle};
}
return book;
})
setBooks(updatedBooks);
}
```
再來就要依靠props system來將function `editBoolById(id, title)`往下傳給BookEdit了。
傳到最後,我們來看看`BookEdit.js`的情況。
```javascript!
import { useState } from "react";
function BookEdit({book, onEdit}){
const [title, setTitle] = useState(book.title);
const handleChange = (event) => {
setTitle(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
onEdit(book.id, title);
};
return(
<form onSubmit={handleSubmit} className="book-edit">
<label>Title</label>
<input className="input" value={title} onChange={handleChange} />
<button className="button is-primary">
Save
</button>
</form>
);
}
export default BookEdit;
```
但是到這邊會發現,我們打上新title,按下save按鈕之後,BookEdit這個元件、整個form我們沒有隱藏起來啊!所以他還是會顯示在螢幕上。除非我們再次按下右上角的鉛筆按鍵,才能更新title。
所以再來我們要處理得就是按下save按鈕之後,要把BookEdit藏起來,這將在下章節說明。
****
## 6-111. Closing the Form on Submit
老師提到會有兩種方法可以處理,他在這個章節會先用初學者的作法教,再來下個章節會告訴我們如何用更好的方法來處理。
但既然是不好的方法了,這個章節我也懶得做筆記了。
****
## 6-112. A Better Solution!
上一章節的作法簡單來說,就是多一個onSubmit prop來處理使用者按下save按鈕的狀況。但他執行的function事實上跟onEdit prop執行的function類似,那這樣我們為什麼還要再多一個prop呢?

於是,我們將兩者的funciton,都塞到同一個handle function中。

****
## 6-114. Adding Images
這裡會放上隨機的照片,讓整體看起來更真實,運用的照片網址:[https://picsum.photos/](https://picsum.photos/)。
網頁上有告訴我們該如何使用該網站:

引入"BookShow.js"之後,會發現照片好像都會重複?不過打開devtool,新增書籍上去時,照片基本上不太會重複?這是什麼原因呢?
老師說是因為打開devtool的時候,可能「disable cache」這個選項被勾選起來了。

而被勾選起來後,代表說不要cache,也就是每次我們新增書籍tilte時,他不會記得上次的照片是什麼,因此又重新request,並且重新獲得心得response。
但總不可能所有使用者都打開著dev tool吧,所以我們在往下看picsum網頁的文件,可以看到這個:

也就是說範例中picsum的位置,可以是隨意的內容,而這隨意的內容將讓我們可以取得不同的圖片,因此在此專案中,我們可以用book.id來取代。
作法如下:
```javascript!
src={`https://picsum.photos/seed/${book.id}/300/200`}
```