# React(MRWR)第 11 節: Mastering the State Design Process
> Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/)
`20230921Thu.~20230929Fri.`
:::danger
11-182 boolean expressions(||、&&)
11-184 把event handler丟在mapping func 外面
:::
## 11-174. Project Organization
這個章節,老師主要提專案的組織架構,這並不是唯一的答案,也不一定要這麼做,不過可以讓我們對於整個專案更清楚了解
基本上分成兩個資料夾:components跟pages:

****
## 11-175. Refactoring with Organization
接著要對第10節的檔案重構整個組織架構,我們先看原先的檔案們的關係:

然後向先前說的,建立兩種資料夾:components跟pages

****
## 11-176. Component Overview
再來要實作accordion(手風琴)的元件,如果能做出一個accordion元件,就可以重複使用,這樣非常的省事,畢竟實作一個accordion非常費勁的。
那麼,可以先看看一個accordion的整體架構,大致分為一個Label搭配一個content:

於是,我們利用陣列的方式,建立名為Label跟Content的keys為一組物件,並放入一個陣列中,作為props可以重複使用:

****
## 11-177. Component Setup
首先,要在components的資料夾中,建立一個檔案叫做"Accoridion.js",裡面的內容一如往常一樣去設置:
**Accordion.js**
```javascript!
function Accordion(){
return <div />
}
export default Accordion;
```
接著來到App.js檔案中,先前已經把App.js裡面所有的內容全數移動至新檔案ButtonPage.js的檔案中了,所以可以放心把App.js裡的內容全部刪掉,然後重新設置新內容:
**App.js**
```javascript!
import Accordion from "./components/Accordion";
function App(){
const items = [
{
label: "Can I use React on a project ?",
content: "You can use it on a project."
},
{
label: "Can I use JavaScript on a project ?",
content: "You can use it on a project."
},
{
label: "Can I use CSS on a project ?",
content: "You can use it on a project."
}
]
return <Accordion items={items}/>
}
export default App;
```
items為一個陣列,裡面共放了3個物件,用陣列包物件主要是我們可以把一組物件作為一個element,後續可以利用array method,去處理這一個個的element。
可以看見上方App元件為父元件,Accordion元件為其子元件,故我們將items prop從App元件傳入之後,要來到Accordion.js中,將items prop作為參數傳入Accordion()中:
```javascript!
function Accordion({items}){
return <div />
}
export default Accordion;
```
****
## 11-178. Reminder on Building Lists
再來希望可以把從App.js傳入的items prop活用到建立實體的Accordion之上,所以我們需要運用map()來幫助我們做轉換。
**Accordion.js**
```javascript!
function Accordion({items}){
const renderedItems = items.map((item) => {
return(
<div>
<div>{item.label}</div>
<div>{item.content}</div>
</div>
);
})
return <div>{rederedItems}</div>
}
export default Accordion;
```
我們來看看現在網頁顯示的內容,看起來完全沒有任何問題:

但是當我們打開console,會發現有warning:
```!
Warning: Each child in a list should have a unique "key" prop.
```

> 參考資料:[Keeping list items in order with key](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key)
上述參考資料也有提到:
```!
JSX elements directly inside a map() call always need keys!
```
所以我們回到App.js檔案中,未每個陣列中的物件新增獨一無二的id
**App.js**
```javascript!
function App(){
const items = [
{
id: "1",
label: "Can I use React on a project ?",
content: "You can use it on a project."
},
{
id: "2",
label: "Can I use JavaScript on a project ?",
content: "You can use it on a project."
},
{
id: "3",
label: "Can I use CSS on a project ?",
content: "You can use it on a project."
}
]
return <Accordion items={items}/>
}
```
之後再回到According.js檔案中,傳入id prop等於item物件的id:
**According.js**
```javascript!
<div key={item.id}>
<div>{item.label}</div>
<div>{item.content}</div>
</div>
```
解決之後,warning就會消失了。
****
## 11-180. State Design Process Overview

### What state + event handlers are there?
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">1. List out what a user will do and changes they will see while using your app</h3>
</div>
這邊以我們實作Accordion為例,當使用者做什麼動作,我們的Accordion會有什麼樣的反應?我們必須針對這個問題,詳細的條列出來。

當點擊first section、second section時,accordion也將有相同的反應。
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">2. Categorize each step as "state" or "event handler"</h3>
</div>
第二步,我們要從第一步中,區分這些行為、反應是屬於state或是屬於event handler。
| 區分 | 說明 |
| ------------- | ----------------------------------------------------- |
| state | 使用者看到螢幕上會改變的內容,就應該用state來實作。 |
| event handler | 使用者行動觸發事件發生,就應該使用event handler處理。 |
以上一步驟來說明:
1. 使用者點擊了"third section":
用event handler實作
2. 原先的first section關起來:
用state實作
3. 被點擊的third section打開來:
用state實作
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">3. Group common steps. Remove Duplicate. Rewrite Description.</h3>
</div>
1. Combine Steps:再來先把相似的功能放在一起,如下圖:

2. Remove Duplicates:把重複的刪除,如下圖,各自保留一項:

3. Rewrite Desription:對留下來的項目各自重新命名、說明:

### What name and type?
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">4. Look at mock up. Remove or symplify parts that aren't changing.</h3>
</div>
我們先看看accordion設計稿,把那些不會改變的東西給移除,我們只專注於會改變的部份,例如下圖不會改變的就是上方的那些文字,先全部移除掉:

<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">5. Replace remaining elements with text descriptions.</h3>
</div>
將上一步驟中已簡化的設計稿,全部轉化成「用文字形容」:

<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">6. Repeat #4 and #5 with a different variation.</h3>
</div>
不同情況下,重複4、5步驟:

<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">7. Imagine you to write a function that returns the text of steps #5 and #6. In addition to your component props, what other arguments would you need?</h3>
</div>
再來要想像,如何把先前的設計稿轉化為code?
除了原先就得放入的prop(即items,section的id、標題以及內容),還有什麼參數是我們需要的?

expanded的狀態,一次只會發生一個,所以我們可以利用陣列的方式,使用陣列的index來控制哪一個為expanded。
實作出來的感覺大概如下圖所示:

當然還有其他的作法,不過如果把state弄成number的型態,相對來說會更好處理:

### Where's it defined?
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px; background-color: #a4dced;">
<h3 style="margin: 0;">8. Decide where each event handler + state will be defined.</h3>
</div>
最後,要決定把expanded prop定義在哪裡?哪個檔案中?
一般來說可能會像下方一樣思考,會有兩種定義方式:

但其實我們應該以更好的方式去思考這個問題:有哪些除了Accordion以外的元件,也需要知道「哪個item是expanded狀態」這件事的嗎?
有的話,把expanded放到App.js,想用的都可以使用。
但如果這個state只有Accordion元件需要知道,那就放在Accordion.js檔案裡就好。
(因為放在Accordion.js,若有其他元件也須使用,可能是Accordion元件的sibling,而React很難去處理這種狀況,一般React比較擅長處理父子元件間的傳遞。)

至於event handler要定義在什麼地方?通常會被定義在跟event handler改變的state同樣的位置。

他們會是一組的,一起待在同個地方。

****
## 11-181. Finding the Expanded Item
再來就要開始實作accordion了,首先要使用state,就必須要import {useState},並且設定其初始值為0,也就是第一個section為展開的。
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
...
}
export default Accordion;
```
接著宣告一個變數叫做"isExpanded",他被賦予一個boolean值,而判斷的內容則為"index === expandedIndex":
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
...
})
}
export default Accordion;
```
從上述程式碼可知,若isExpanded為true,則該section為展開,反之,若isExpanded為false,則該section的內容不會顯示,而這裡的不會顯示,我們不希望用CSS去隱藏,而是希望直接把整個section的內容移除(也就是整個`<div>...內容...</div>`都移除掉)
****
## 11-182. Conditional Rendering
在開始之前,我們必須先知道一件事,React並不會印出booleans、nulls以及undefined,如下圖可見除了40(number或是string)之外,其餘都不會印出東西。

這裡簡單快速地複習JS的boolean expressions,and跟or:

若利用and 或 or,他們並不會回傳true或false
1. or(||),會回傳第一個為truthy value的內容。
2. and(&&),會回傳第一個為falsy value的內容或者回傳最後一個為truthy value的內容。
簡單的來看一些例子:


所以利用上方的例子,來改變accordion現在是否為展開的:
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
const content = isExpanded && <div>{item.content}</div>
return(
<div key={item.id}>
<div>{item.label}</div>
{content}
</div>
);
})
return <div>{renderedItems}</div>
}
export default Accordion;
```
前後的改變如下所示:

至於content到底會得到什麼,詳見下圖:

不過其實我們甚至可以不必宣告content這個變數,直接把boolean expressions放入到JSX中,如此兩行就能直接縮減為一行:
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
return(
<div key={item.id}>
<div>{item.label}</div>
{isExpanded && <div>{item.content}</div>}
</div>
);
})
return <div>{renderedItems}</div>
}
export default Accordion;
```
****
## 11-183. Inline Event Handlers
下圖是我們希望的情況:

之前的課程我們都是另外宣告一個變數,作為event發生後觸發事件的變數,但其實我們也可以只用一行完成enent handler,兩種方式都可以,主要取決於觸發事件的code是否能一行完成,若可以只用一行完成,則放入onClick後面即可,可參考下方圖片說明:

再來,我們希望點擊section`<div>{item.label}</div>`的部份,會展開section下的內容,因此我們要放onClick在`<div>{item.label}</div>`上面。
**Accordion.js**
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
return(
<div key={item.id}>
<div onClick={() => setExpandedIndex(index)}>{item.label}</div>
{isExpanded && <div>{item.content}</div>}
</div>
);
})
return <div>{renderedItems}</div>
}
export default Accordion;
```
上方程式碼中的`<div onClick={() => setExpandedIndex(index)}>{item.label}</div>`,是利用React中的useState,並放入event handler中,也就是說每當使用者點擊這項div,就會觸發這個事件,去更新setExpandedIndex的state,至於更新成什麼呢?就是傳入的index,而index是由mapping時JS產生給每一項元素的。

****
## 11-184. Variation on Event Handlers
上一節說過,如果event handler裡面的內容過多,我們會傾向把他拉出來建一個function,這樣比較易讀,如下所示:
```javascript!
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
const handleClick = () => {
setExpandedIndex(index);
...
...
...
}
return(
<div key={item.id}>
<div onClick={handleClick}>{item.label}</div>
{isExpanded && <div>{item.content}</div>}
</div>
);
})
```
但是如果我們有多個event handler外,每個handler又有很多行程式要去處理,整個mapping funciton裡面會變得很雜亂,如下圖:

所以這邊要來做點變化,把event handler移到mapping function的外面。
我們先直接把handleClick往map()外面移動,這裡很明顯的會出現錯誤,畢竟handleClick裡面需要index這個變數,但是index是map()產生的,map()之外當然無法取用。

```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
//handleClick一道外面無法使用index
const handleClick = () => {
setExpandedIndex(index);
}
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
return(
<div key={item.id}>
<div onClick={handleClick}>{item.label}</div>
{isExpanded && <div>{item.content}</div>}
</div>
);
})
return <div>{renderedItems}</div>
}
export default Accordion;
```
但我們還是希望可以把event handler放到mapping funciton之外,這樣才可以避免太過雜亂,那麼現在的問題就是,我們該如何把event handler放到外面去,又可以取得index變數?
解決方法就是:依靠enent handler的longhand version + shorthand version。


PS 這裡取名為index以及nextIndex主要目的是為了區別這些是不同的變數而已,事實上我們直接全部都用index也是可以的,但確保自己知道這些名稱雖相同,但意義是不同的。
****
## 11-185. Conditional Icon Rendering
再來想要顯示icon,當section展開時,icon會往下指,反之當section合起來時,icon會往左邊指。
這裡利用了三元運算子,我們先用文字"Down"跟"Left"來代替icon。
```javascript!
import { useState } from "react";
function Accordion({items}){
const [expandedIndex, setExpandedIndex] = useState(0);
const handleClick = (nextIndex) => {
setExpandedIndex(nextIndex);
}
const renderedItems = items.map((item, index) => {
const isExpanded = index === expandedIndex;
const icon = <span>{isExpanded ? "Down" : "Left"}</span>
return(
<div key={item.id}>
<div onClick={() => handleClick(index)}>
{icon}
{item.label}
</div>
{isExpanded && <div>{item.content}</div>}
</div>
);
})
return <div>{renderedItems}</div>
}
export default Accordion;
```
****
## 11-186. Displaying Icons
接著要顯示出icon,而非繼續用文字代替,icon部份運用先前課堂中使用過得[React icon](https://react-icons.github.io/react-icons/)。
我們這邊想使用的是[Github Octicons icons](https://react-icons.github.io/react-icons/icons?name=go)中的GoChevronLeft、GoChevronDown icon樣式。
用法就是要先import,import後方寫入要使用的icon名稱,剩下格式照每種不同而變動:
```javascript!
import { GoChevronLeft, GoChevronDown } from "react-icons/go";
```
記得GoChevronLeft跟GoChevronDown兩個icon import進來檔案之後,他們都是component元件,所以我們可以對待他們像是對待元件一樣,所以我們稍微修改一下上一節icon變數:
```javascript!
//原先樣子
const icon = <span>{isExpanded ? "Down" : "Left"}</span>
//放上react icon元件
const icon = <span>{isExpanded ? <GoChevronDown /> : <GoChevronLeft />}</span>
```
****
## 11-187. Adding Styling
上一節加上react icon元件之後,位置不太正確,所以這個章節將要去修正它。

這邊將使用tailwind css,利用className引入style。
首先,先幫包住label加上icon的`<div>`加上樣式:
```javascript!
<div className="flex p-3 bg-gray-50 border-b items-center cursor-pointer" onClick={() => handleClick(index)}>
{icon}
{item.label}
</div>
```
接下來,對包住content的`<div>`加上樣式:
```javascript!
{isExpanded && <div className="border-b p-5">{item.content}</div>}
```
再來,對包住整個accordion的`<div>`加上樣式:
```javascript!
return <div className="border-x border-t rounded">{renderedItems}</div>
```
另外,若覺得react icon太小的話,可以對`<span>`加上樣式:
```javascript!
const icon = <span className="text-2xl">{isExpanded ? <GoChevronDown /> : <GoChevronLeft />}</span>
```
****
## 11-188. Toggling Panel Collapse
目前我們的預設是如下,也就是說每次打開來,第一個section就必定是展開的:
```javascript!
const [expandedIndex, setExpandedIndex] = useState(0);
```
不過我們如果希望全部預設關起來呢?這就要改成-1,如下所示:
```javascript!
const [expandedIndex, setExpandedIndex] = useState(-1);
```
另外,我們也希望,展開的section若再點擊一次的話可以變成合併的,因此我們需要在handleClick()裡面加一點logic判斷:
```javascript!
const handleClick = (nextIndex) => {
if(nextIndex === expandedIndex) setExpandedIndex(-1);
else setExpandedIndex(nextIndex);
}
```
****
## 11-190. [Optional] Delayed State Updates
## 11-191. [Optional] Functional State Updates

