---
# System prepended metadata

title: React(MRWR)第 11 節： Mastering the State Design Process
tags: [React, Udemy]

---

# 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：
![](https://hackmd.io/_uploads/rk-71LTy6.png)

****

## 11-175. Refactoring with Organization
接著要對第10節的檔案重構整個組織架構，我們先看原先的檔案們的關係：
![](https://hackmd.io/_uploads/BJmqkUpya.png)

然後向先前說的，建立兩種資料夾：components跟pages
![](https://hackmd.io/_uploads/S1q6G8pya.png)


****

## 11-176. Component Overview
再來要實作accordion（手風琴）的元件，如果能做出一個accordion元件，就可以重複使用，這樣非常的省事，畢竟實作一個accordion非常費勁的。

那麼，可以先看看一個accordion的整體架構，大致分為一個Label搭配一個content：
![](https://hackmd.io/_uploads/SkbODIak6.png)

於是，我們利用陣列的方式，建立名為Label跟Content的keys為一組物件，並放入一個陣列中，作為props可以重複使用：
![](https://hackmd.io/_uploads/S1kytUTJa.png)

****

## 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;
```

我們來看看現在網頁顯示的內容，看起來完全沒有任何問題：
![](https://hackmd.io/_uploads/ryuu1Pa1a.png)

但是當我們打開console，會發現有warning：
```!
Warning: Each child in a list should have a unique "key" prop.
```
![](https://hackmd.io/_uploads/SJi3JPa1a.png)

> 參考資料：[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
![](https://hackmd.io/_uploads/S1RvswaJ6.png)

### 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會有什麼樣的反應？我們必須針對這個問題，詳細的條列出來。

![](https://hackmd.io/_uploads/BJ0RWuaJa.png)

當點擊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：再來先把相似的功能放在一起，如下圖：
![](https://hackmd.io/_uploads/ryVRkYRya.png)

2. Remove Duplicates：把重複的刪除，如下圖，各自保留一項：
![](https://hackmd.io/_uploads/H1g_eKA1p.png)

3. Rewrite Desription：對留下來的項目各自重新命名、說明：
![](https://hackmd.io/_uploads/B13ClKCJ6.png)

### 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設計稿，把那些不會改變的東西給移除，我們只專注於會改變的部份，例如下圖不會改變的就是上方的那些文字，先全部移除掉：
![](https://hackmd.io/_uploads/SJESQFR16.png)

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

將上一步驟中已簡化的設計稿，全部轉化成「用文字形容」：
![](https://hackmd.io/_uploads/HkgqDtR1p.png)

<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步驟：
![](https://hackmd.io/_uploads/SkL0OFRkT.png)

<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、標題以及內容），還有什麼參數是我們需要的？

![](https://hackmd.io/_uploads/r1ddWqCy6.png)

expanded的狀態，一次只會發生一個，所以我們可以利用陣列的方式，使用陣列的index來控制哪一個為expanded。

實作出來的感覺大概如下圖所示：
![](https://hackmd.io/_uploads/r1Ucz9AJT.png)

當然還有其他的作法，不過如果把state弄成number的型態，相對來說會更好處理：
![](https://hackmd.io/_uploads/Hkwk7qCkT.png)

### 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定義在哪裡？哪個檔案中？
一般來說可能會像下方一樣思考，會有兩種定義方式：
![](https://hackmd.io/_uploads/SJHom5R1p.png)

但其實我們應該以更好的方式去思考這個問題：有哪些除了Accordion以外的元件，也需要知道「哪個item是expanded狀態」這件事的嗎？

有的話，把expanded放到App.js，想用的都可以使用。
但如果這個state只有Accordion元件需要知道，那就放在Accordion.js檔案裡就好。
（因為放在Accordion.js，若有其他元件也須使用，可能是Accordion元件的sibling，而React很難去處理這種狀況，一般React比較擅長處理父子元件間的傳遞。）
![](https://hackmd.io/_uploads/r1CTQ5CJp.png)

至於event handler要定義在什麼地方？通常會被定義在跟event handler改變的state同樣的位置。
![](https://hackmd.io/_uploads/SkSXuc0Ja.png)

他們會是一組的，一起待在同個地方。
![](https://hackmd.io/_uploads/HyOVOc0ka.png)

****

## 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）之外，其餘都不會印出東西。
![](https://hackmd.io/_uploads/rJoUp9R1p.png)

這裡簡單快速地複習JS的boolean expressions，and跟or：
![](https://hackmd.io/_uploads/HJHTa9Cya.png)

若利用and 或 or，他們並不會回傳true或false
1. or(||)，會回傳第一個為truthy value的內容。
2. and(&&)，會回傳第一個為falsy value的內容或者回傳最後一個為truthy value的內容。

簡單的來看一些例子：
![](https://hackmd.io/_uploads/r1pyyi01T.png)

![](https://hackmd.io/_uploads/rkbV1jC1a.png)

所以利用上方的例子，來改變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;
```

前後的改變如下所示：
![](https://hackmd.io/_uploads/ByZ0MsA1p.png)

至於content到底會得到什麼，詳見下圖：
![](https://hackmd.io/_uploads/S10lmsCkp.png)

不過其實我們甚至可以不必宣告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
下圖是我們希望的情況：
![](https://hackmd.io/_uploads/HJErdPGx6.png)

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

再來，我們希望點擊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產生給每一項元素的。

![](https://hackmd.io/_uploads/H1qisDGep.png)

****

## 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裡面會變得很雜亂，如下圖：
![](https://hackmd.io/_uploads/HkWmqOze6.png)

所以這邊要來做點變化，把event handler移到mapping function的外面。

我們先直接把handleClick往map()外面移動，這裡很明顯的會出現錯誤，畢竟handleClick裡面需要index這個變數，但是index是map()產生的，map()之外當然無法取用。

![](https://hackmd.io/_uploads/ByiyjdMlp.png)

```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。
![](https://hackmd.io/_uploads/S1JltDGl6.png)

![](https://hackmd.io/_uploads/Skeda_fla.png)

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元件之後，位置不太正確，所以這個章節將要去修正它。

![](https://hackmd.io/_uploads/B1hH31Vea.png)

這邊將使用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
![](https://hackmd.io/_uploads/rJiJOlNl6.png)

![](https://hackmd.io/_uploads/S1qFOl4ep.png)
