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