#React(MRWR)第 4 節: State: How to Change Your App > Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/) `20230725Tue.~20230802Wed.` :::danger 重要章節: 4-47 useState() 4-51 spread operator 4-52 map() ::: ## 4-41. Initial App Setup 先來看看第4節將要製作的專案內容。 ![](https://hackmd.io/_uploads/S1q9KmT92.png) 可以從上圖知道,這次專案中會有parent component(App.js)以及child component(AnimalShow.js) **專案功能:** 一開始,畫面上只會看見一個按鈕「Add Animal」,當點擊一次,便會出現一種動物with愛心,而愛心經點擊後會變大。 1. 一開始,沒有任何動物出現 ![](https://hackmd.io/_uploads/rkuiqQach.png) 2. 點擊2次後,出現不同的動物 但希望使用相同的component(AnimalShow),經由不同的props來達到跑出不同的動物的目的。 ![](https://hackmd.io/_uploads/BySzsX693.png) **專案Set Up:** 1. 同之前的專案,先把`src`資料夾中的所有資料給刪除。 2. 建立三個JS檔案在`src`中:`index.js`、`App.js`、`AnimalShow.js` 3. **index.js** ```javascript= import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; const el = document.getElementById("root"); const root = ReactDOM.createRoot(el); root.render(<App />); ``` **** ## 4-42. Introducing the Event System 接下來要完成的目標,是每點擊按鈕「Add Animal」,數字就會增加1。 ![](https://hackmd.io/_uploads/rkumA7Tq2.png) 在這裡先提到React有兩種系統: 1. **Event System** > Detect a user clicking the button 2. **Status System** > Update content on screen **** ## 4-43. Events in Detail ![](https://hackmd.io/_uploads/Bk1EUplj2.png) 這章節主要提到event system。 **1. 決定想要怎麼樣的事件** React中支援的event可以參考[官方文件](https://zh-hant.legacy.reactjs.org/docs/events.html)(課程提供的是舊版的,新版的在[這裡](https://react.dev/reference/react-dom/components/common#common-props)) 其中舉兩個較常見的例子: ![](https://hackmd.io/_uploads/BkwykCesn.png) **2. 建立function** 建立的function,一般我們會稱其為"event handler"或"callback function"。 e.g. ```javascript= const handleClick = () =>{ console.log("Button was Clicked!!!") } ``` **3. function的命名** 這沒有強制規定,但大多數會遵循一個模板,也就是寫成「handle+event的名稱」。 如第二步驟中的例子"handleClick"。 **4. 傳遞function** 將function透過props system傳入明確的element(如`<div>`、`<button>`...) e.g.底下的button傳入prop名為`onClick={handleClick}` ```javascript= function App(){ const handleClick = () =>{ console.log("Button was Clicked!!!") } return( <div> <button onclick={handleClick}>Add Animas!</button> </div> ) } export default App; ``` **5. 確保使用有效的event name** 其中的onClick,即[這裡](https://react.dev/reference/react-dom/components/common#common-props)React中支援的event name。 **6. 確保傳入的是function的參考** event name後方傳入的是function 的名稱,而非呼叫函式(如`handleClick()`) ```javascript= onClick={handleClick} ``` **** ## 4-46. Introducing the State System 透過event system,我們可以捕捉到user做了什麼事情,進而去觸發function。而想要更改螢幕上的內容,就得看看state system的部份了。 在此專案中,我們的想法是,當我們點擊按鈕,數字就會增加1,如下圖所示: ![](https://hackmd.io/_uploads/BJbiR0gs2.png) 了解我們希望呈現的方式之後,開始思考如何達成目的,如下圖所示: ![](https://hackmd.io/_uploads/ryOzk1-i3.png) **** ## 4-47. More on State > 完整資料: > [官方文件State: A Component's Memory](https://react.dev/learn/state-a-components-memory) 接續上一章節,這裡繼續談論State。 ![](https://hackmd.io/_uploads/HJOF7kWs2.png) 基本上可以分為4個步驟: ![](https://hackmd.io/_uploads/SkvAlebjn.png) **1. 先import useState到檔案中** ```javascript= import { useState } from 'react'; ``` **2. 利用useState function 定義state與setter function** 其中,index是state的變數、setIndex是setter function。而`[ ]`中括號的語意代表著陣列解構(array destructuring) ```javascript= const [index, setIndex] = useState(0); ``` ![](https://hackmd.io/_uploads/Sy0Brgbs3.png) :::info The useState Hook provides those two things: 1. A state variable to retain the data between renders. 2. A state setter function to update the variable and trigger React to render the component again. ::: :::warning **陣列解構(array destructuring)** 可參考: 1. [Danny文章](https://ithelp.ithome.com.tw/articles/10292493) 2. [Destructuring assignment](https://javascript.info/destructuring-assignment)<---官網提供的 ::: **3. 將state(此例的state為index)運用在component中** **4. 當使用者觸發某事,則去更新state(此例的state為index)** **** ## 4-49. Why Array Destructuring? 先說結論,利用Array Destructuring是為了簡化code。 若不使用Array Destructuring,useState可能會長底下的樣子(useState的物件為假設) ```javascript function useState(defaultValue){ return{ yourState: defaultValue, yourSetter: () => {} } } function App(){ const stateConfig = useState(0); const count = stateConfig.yourState; const setCount = stateConfig.yourSetter; const handleClick = () => { setCount(count + 1); } return( <div> <button onClick={handleClick}>Add Animas!</button> <div>Number of Animals: {count}</div> </div> ) } ``` 可以看見中間useState若沒使用array destructuring,則需要三行來完成。(如下所示) ```javascript const stateConfig = useState(0); const count = stateConfig.yourState; const setCount = stateConfig.yourSetter; ``` 不過若我們使用array destructuring,則只需要短短一行即可解決: ```javascript const [count, setCount] = useState(0); ``` 這就是為什麼使用陣列解構的原因,為了簡潔、便利。 **** ## 4-50. Back to the App 這章節老師大略提到該怎麼實作,簡單來說就是要寫一個陣列,然後當使用者點擊按鈕之後,就會更新動物名稱進到陣列中。(如下圖所示) 一開始尚未點擊按鈕的狀態: ![](https://hackmd.io/_uploads/HJBqut7sn.png) 點擊兩次按鈕的狀態: ![](https://hackmd.io/_uploads/BJyh_Y7i2.png) **** ## 4-51. Picking a Random Element 這章節要來寫一個可以隨機取得一種動物的function。 首先,要設立一個變數animals作為陣列,裡面當作一個pool,放置所有想讓使用者取得的動物名稱。 ```javascript= function getRandomAnimal(){ const animals = ['bird', 'cat', 'cow', 'dog', 'gator', 'horse'] } ``` 然後我會希望這個function可以return陣列中的任意動物,因此我們會想讓return值為`animal[隨機數字]`。其中, 1. Math.floor() 函式會回傳小於等於所給數字的最大整數。 2. Math.random() 會回傳一個偽隨機小數 (pseudo-random) 介於 0 到 1 之間(包含 0,不包含 1) **App.js** ```javascript! function getRandomAnimal(){ const animals = ['bird', 'cat', 'cow', 'dog', 'gator', 'horse'] return animals[Math.floor(Math.random() * animals.length)]; } ``` 接著就要運用到React中的useState了,要記得若想更新state,一定使用setter去更新。 前面已實做出一個`getRandomAnimal`的function。我們希望的作法是,每點擊一次按鈕,就會觸發`handleClick` function,而`handleClick` function便會隨機新增一種動物進我們的aniamls陣列(aniamls陣列為一個state)當中,這個動作將透過useState的setter功能來實作,這裡我們命名做`setAnimals`。 **App.js** ```javascript! function getRandomAnimal(){ const animals = ['bird', 'cat', 'cow', 'dog', 'gator', 'horse'] return animals[Math.floor(Math.random() * animals.length)]; } function App(){ const [animals, setAnimals] = useState([]); const handleClick = () =>{ setAnimals([...animals, getRandomAnimal()]); } return( <div> <button onClick={handleClick}>Add Animas!</button> <div>{animals}</div> </div> ) } ``` **圖文解說** **App.js** ![](https://hackmd.io/_uploads/ryDbp9Xsh.jpg) 其中,可以注意到他使用到了「展開運算子(Spread Operator)」,可參考[這篇文章](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/rest_spread.html)。裡面有提到「展開運算符可以作陣列的淺拷貝」,至於淺拷貝可以參考[這篇文章](https://medium.com/andy-blog/%E9%97%9C%E6%96%BCjs%E4%B8%AD%E7%9A%84%E6%B7%BA%E6%8B%B7%E8%B2%9D-shallow-copy-%E4%BB%A5%E5%8F%8A%E6%B7%B1%E6%8B%B7%E8%B2%9D-deep-copy-5f5bbe96c122)。 :::info 再次複習useState(存狀態)。 當使用useState,代表你告訴React,你想要這個component去記住一些事情。 ```javascript! const [animals, setAnimals] = useState([]); ``` 以上方為例,這代表你希望React記住`animals`,並且useState()的唯一參數,即`animals`的初始值,此處範例便可知`animals`的初始值為一個空陣列。 `setAnimals`便是setter,用於更新state `animals`的。 ::: **** ## 4-52. List Building in React 再來我們要創建list,list在很多應用程式中都非常常見。例如Gmail顯示mails、Youtube顯示影片清單連結、FaceBook顯示廣告或post等等的清單、snapchat顯示你收到的訊息的清單。list這個想法存在幾乎每一種應用程式裡面。 **實作想法** 在前面章節中,我們已經利用陣列實作出一個動物的清單。接著,我們希望可以將該陣列中的動物們,轉換進component之中。實作想法如下圖: ![](https://hackmd.io/_uploads/Sy7T2jQjn.jpg) 而上圖中提到的magic transform step,事實上可以運用JS的語法達成,也就是"map function"。 > 關於map的運用,可以直接參考阿傑的文章[Day 11 咩色用得好 - Array.prototype.map](https://ithelp.ithome.com.tw/articles/10299099)。 > > 發現除了有map()之外,JS中還有個Map constructor,Map constructor與object相似,皆擁有key value pair。 圖中的magic transform step,若寫成程式則如下所示: ```javascript! const renderedAnimals = animals.map((animal, index)=>{ return <AnimalShow type={animal} key={index} />; }); ``` 圖片說明: ![](https://hackmd.io/_uploads/BkIyiIIsn.jpg) 具體化說明: ![](https://hackmd.io/_uploads/BkUko8Iih.jpg) 所有的程式碼則如下所示: **App.js** ```javascript! import {useState} from "react"; import AnimalShow from "./AnimalShow"; function getRandomAnimal(){ const animals = ['bird', 'cat', 'cow', 'dog', 'gator', 'horse'] return animals[Math.floor(Math.random() * animals.length)]; } console.log(getRandomAnimal()); function App(){ const [animals, setAnimals] = useState([]); const handleClick = () =>{ setAnimals([...animals, getRandomAnimal()]); }; const renderedAnimals = animals.map((animal, index)=>{ return <AnimalShow type={animal} key={index} />; }); return( <div> <button onClick={handleClick}>Add Animas!</button> <div>{renderedAnimals}</div> </div> ); } export default App; ``` **** ## 4-54. Loading and Showing SVGs 接著,這個章節要加入圖片。 <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step1. 將圖片import進檔案</h3> </div> 首先,將圖片import進檔案,同時將圖片給各自命名: **AnimalShow.js** ```javascript! import bird from './svg/bird.svg'; import cat from './svg/cat.svg'; import cow from './svg/cow.svg'; import dog from './svg/dog.svg'; import gator from './svg/gator.svg'; import horse from './svg/horse.svg'; ``` 至於bird, cat, cow...這些變數代表著什麼,可以印出來看看,也可以參考[3-34筆記](https://hackmd.io/yIqdj6sHS7OGPSN38Z_Miw?view#3-34-Including-Images)。不過可以知道的是這些變數都被賦予了「字串(string)」 <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step2. 建立物件放圖片</h3> </div> 將這些圖片放進一個物件中: **AnimalShow.js** ```javascript! const svgMap = { bird, cat, cow, dog, gator, horse }; ``` 事實上,如果展開這個物件來看,他會長這樣: ![](https://hackmd.io/_uploads/Hkp4Arwo2.png) <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step3. 建立component放圖片</h3> </div> 再來,要建立一個component,將動物圖片顯示到螢幕上。 此處的`type`是props物件中的屬性之一,資料會透過parent component(App.js)傳給child component(AnimalShow.js)。 ```javascript! function AnimalShow({type}){ return( <div> <img alt = "animal" src = {svgMap[type]} /> </div> ) } ``` 這裡可以注意到,一般物件取值有兩種方式,透過`.`(dot)以及`[ ]`(brackets),但是我們這裡物件取值必須只能使用`[ ]`(brackets),因為我們是利用變數'type'取物件的值。 可以參考[[JavaScript] 何謂物件取值?在什麼時機上會用到?點(.)和方括號([])取值的不同之處](https://hackmd.io/@peter77730/BkOHgKBUK?utm_source=preview-mode&utm_medium=rec)這篇文章。裡面提到,大致上遇到以下三種狀況,僅能使用`[ ]`(brackets)取值: 1. 以「變數」取值 2. 屬性開頭是「數字」 3. 新增特殊字元 這裡遇到的狀況便是上述第一種,也因為這裡是變數取值,所以type不需要再用單引號(`''`)或雙引號(`""`)包起來。 不過如果例如用法是`svgMap['bird']`,就要記得用單引號(`''`)或雙引號(`""`)。 **** ## 4-55. Increasing Image Size 這章節要加上愛心,當我們點擊動物圖片時,這個愛心就會變大。而在React中,若要記得每次更新後的狀態,便需要使用到useState方法。 <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step1. import useState</h3> </div> 使用useState之前,記得要先import進檔案中: **AnimalShow.js** ```javascript! import {useState} from 'react'; ``` <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step2. 新增onClick事件&觸發後要執行的函式</h3> </div> 我們會把愛心和動物放在同一個`<div>`中,所以我們要把onClick 事件加到`<div>`上。每當onClick事件被觸發,就要去執行handleClick function。 **AnimalShow.js** ```javascript! function AnimalShow({type}){ const handleClick = () =>{ } return( <div onClick={handleClick}> <img alt = "animal" src = {svgMap[type]} /> </div> ) } ``` <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">step3. 設置state</h3> </div> 希望點擊一次,點擊次數就加1。利用useState(),並在handleClick function中用setter改變state。 **AnimalShow.js** ```javascript! function AnimalShow({type}){ const [clicks, setClicks] = useState(0); const handleClick = () =>{ setClicks(clicks + 1); } return( <div onClick={handleClick}> <img alt = "animal" src = {svgMap[type]} /> </div> ) } ``` <div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px"> <h3 style="margin: 0">ste4. 加入heart圖片並給予style</h3> </div> props物件中的屬性style值得注意,style會是一個物件,在JSX中屬性的value若有變數(此處變數為clicks),其value要再用`{ }`(curly braces)包起來,因此看起來會是兩層curly braces`{{ }}`。 也可以從底下程式碼得知,尚未點擊時,clicks的初始值為0(因為useState(0)),每點擊一次就會增加10。 最後記得加上單位'px'。 **AnimalShow.js** ```javascript! function AnimalShow({type}){ const [clicks, setClicks] = useState(0); const handleClick = () =>{ setClicks(clicks + 1); } return( <div onClick={handleClick}> <img alt = "animal" src = {svgMap[type]} /> <img alt = "heart" src = {heart} style = {{ width: 10 + 10 * clicks +'px' }} /> </div> ) } ``` **** ## 4-56. Adding Custom CSS 先建立兩個css檔案。 ![](https://hackmd.io/_uploads/r1sFmwPj2.png) 建立css檔案之外還不夠,因為此時的css檔案不起任何作用,所以還必須將css檔import進要使用該css檔的js檔中。 **App.js** ```javascript import './App.css' ``` **AnimalShow.js** ```javascript! import './AnimalShow.js' ``` 可以看到import時,並沒有命名任何的變數,這是因為我們想使用整個css檔案的內容。 複習可參考[2-19](https://hackmd.io/fxY8uO0fTpa9XBizBKTYWg#2-19-Applying-Styling-in-JSX)。在HTML檔案中,若要使用css的class,用法如下: **A.css** ```css! .item{ display: inline; color: white; background-color: black; } ``` **A.html** ```htmlembedded! <li class="item">first</li> ``` 但如果換作是JSX來寫,則class要改成className。 ```htmlembedded! <li className="item">first</li> ```