# 從 Hooks 開始,讓你的網頁 React 起來系列的閱讀筆記(7-12) ###### tags: `閱讀筆記` `前端筆記` `React` ## 完成章節 - 7-12(2021/11/23) ## 事件函式的處理 有別於我一開始寫的方式(把加跟減的任務拆成兩個函式),看到文章內也有使用一個函式包著兩個任務(再經判斷執行不同的任務)。 ```javascript= // 分別用兩個不同的函式 const Content = () => { const [count, setCount] = useState(5); // 兩個獨立的函式代表兩個不同的任務 function add () { setCount(count + 1) } function minus () { setCount(count - 1) } return ( <div className='container'> <div className='chevron chevron-up' // 點擊事件 onClick={add} style={{ visibility: count >= 10 && 'hidden' }} /> <div className="number">{count}</div> <div className='chevron chevron-down' // 點擊事件 onClick={minus} style={{ visibility: count <= 0 && 'hidden' }} /> </div> ) }; ReactDOM.render( <Content />, document.querySelector('#root') ); ``` 道理和 Vanilla JS 寫 `addEventListener` 的原理類似。 - 在 Vanilla JS 的 `addEventListener` 不用在 callback 後面寫括號 (),因為函式後面接 () 代表是叫用函式 - 但是是要由事件觸發後執行函式,所以只要在 callback 寫函式名稱就好 ```javascript= // 假設有一個按鈕 const button = document.querySelector('#my-button'); function clickHandler () { console.log('我被點了'); } button.addEventListener('click', clickHandler); // 不是這樣子喔,這樣子代表函式直接執行,並不是透過觸發事件執行 button.addEventListener('click', clickHandler()); ``` - 所以依照這個原理「觸發事件後再執行函式」,可以發展出函式包兩個任務 ```javascript= // 文章內的方法,用一個函式包覆著兩個函式(任務) const Content = () => { const [count, setCount] = useState(5); function clickHandler (type) { return function () { if (type === 'add') { setCount(count + 1); } else if (type === 'minus') { setCount(count - 1); } } } return ( <div className='container'> <div className='chevron chevron-up' // 點擊事件 // clickHandler 已經先執行,但是是回傳另一個函式,所以這時候可以想成 onClick={clickHandler 回傳的 function...} // 只要事件觸發(點擊 onClick)就會執行 clickHandler 回傳的函式 onClick={clickHandler('add')} style={{ visibility: count >= 10 && 'hidden' }} /> <div className="number">{count}</div> <div className='chevron chevron-down' // 點擊事件 // clickHandler 已經先執行,但是是回傳另一個函式,所以這時候可以想成 onClick={clickHandler 回傳的 function...} // 只要事件觸發(點擊 onClick)就會執行 clickHandler 回傳的函式 onClick={clickHandler('minus')} style={{ visibility: count <= 0 && 'hidden' }} /> </div> ) }; ReactDOM.render( <Content />, document.querySelector('#root') ); ``` ### 為什麼回傳的函式可以讀取 `type` Arguments? 因為是靜態範疇(Static Scope)+ 函式範疇(Function Scope)的緣故,**變數及函式在宣告的當下就決定自己的可見範圍了,而且 return 的函式確實是在 `clickHandler` 裡面,所以該函式內找不到的東西就會往外層找。** 所以當事件觸發任務(函式)的時候 return 的函式就會被叫用,然後就會往外層找 `type` Argument了。 ## 使用迴圈建立數個相同的 Component 除了自己土法煉鋼地用手貼上數個 Component,也可以使用 JSX 也可以使用迴圈達到同樣子的效果。 ```javascript= function Content = () => { // component stuff... } ReactDOM.render( // 記得每個 Component 之外一定要有一個 wrapper(包裝紙) <div style={{ display: 'flex', flexWrap: 'wrap' }}> // 土法煉鋼狂貼之術 <Content /> <Content /> <Content /> <Content /> </div> , document.querySelector('#root') ); ``` 使用迴圈來建立數個相同的 Component - 因為 JSX 是執行 expression(表達式)所以不能用一般的 `for loop`,`foor loop` 是 statement(陳述式),不是 expression(表達式) - 可以用 `Array.from()` 快速建立一組陣列,之後再搭配 `arr.map()` [Array.from()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) ```javascript= const { useState } = React; const contentQty = Array.from({ length: 4 }, (value, indexNum) => indexNum); console.log(contentQty) // Array(4)[0, 1, 2, 3] function Content = () => { // component stuff... }; ReactDOM.render( // 記得每個 Component 之外一定要有一個 wrapper(包裝紙) <div style={{ display: 'flex', flexWrap: 'wrap' }}> {contentQty.map(value => { // 每次迴圈都會產生一個 <Content /> return <Content /> })} </div> , document.querySelector('#root') ); ``` ### 為什麼把 `const contentQty = Array.from({ length: 4 }, (value, indexNum) => indexNum);` 放在 Component 函式裡面會出現問題? 我一開始是放在 Component 的函式之內,結果報錯了。 ```javascript= const Content = () => { const contentQty = Array.from({ length: 4 }, (value, indexNum) => indexNum); // other stuff... return ( <div className='container'> <div className='chevron chevron-up' onClick={clickHander('add')} style={{ visibility: count >= 10 && 'hidden' }} /> <div className="number">{count}</div> <div className='chevron chevron-down' onClick={clickHander('minus')} style={{ visibility: count <= 0 && 'hidden' }} /> </div> ) }; ReactDOM.render( <div style={{ display: 'flex', flexWrap: 'wrap' }}> {contentQty.map(value => { // Uncaught ReferenceError: contentQty is not defined return <Content /> })} </div> , document.querySelector('#root') ); ``` ![](https://i.imgur.com/McNoNYT.png) 原因是 Content 只是單純地回傳 Component,所以 `contentQty` 出了 `Content` 的函式就掛了。也因為如此在此例才要把 `const contentQty = Array.from({ length: 4 }, (value, indexNum) => indexNum);` 宣告在外層,這樣子 `ReactDOM.render` 才讀取的到 `contentQty`。 ## 元件可以由很多的小元件組成 原本是一整個 CardComponent: ![](https://i.imgur.com/50JTXOB.png) 看起來是一整個密密麻麻的 JSX - 維護麻煩,因為都擠在一起了 ```javascript= function CardComponent () { return ( <div className='container'> <div className='card-header'>Network Speed Converter</div> <div className='card-body'> <div className='unit-control'> <div className='unit'>Mbps</div> <span className='exchange-icon fa-fw fa-stack'> <i className='far fa-circle fa-stack-2x' /> <i className='fas fa-exchange-alt fa-stack-1x' /> </span> <div className='unit'>MB/s</div> </div> <div className='converter'> <div className='flex-1'> <div className='converter-title'>Set</div> <input type='number' className='input-number' min='0' /> </div> <span className='angle-icon fa-2x' style={{ marginTop: 30, }} > <i className='fas fa-angle-right' /> </span> <div className='text-right flex-1'> <div className='converter-title'>Show</div> <input className='input-number text-right' type='text' value='125' disabled /> </div> </div> </div> <div className='card-footer'>FAST</div> </div> ); } ReactDOM.render( <CardComponent />, document.querySelector('#root') ); ``` 把區塊分出來,就可以看到 CardComponent 裡面還有其他小 Component: ![](https://i.imgur.com/SPQ3EeK.png) - 每個區分開的 Component 開頭必須是大寫 - 插入的時候要記得是 `<ComponentName />` 不是 `{ComponentName()}` 這樣子就會變成叫用函式(如果該函式有回傳東西的話就會回傳,如果是回傳 JSX 的話還是會轉譯成 HTML 但是就不是 Component了) ![](https://i.imgur.com/iA7DcEm.png) *寫成 `{CardHeader()}` 後發現 `CardHeader` 不是 Component 了* ![](https://i.imgur.com/eC1nByx.png) *但 `{CardHeader()}` 回傳的 JSX 還是會被轉譯成 HTML* ```javascript= const { useState } = React; function CardHeader () { return ( <div className='card-header'>Network Speed Converter</div> ); } function CardUnitControl () { return ( <div className='unit-control'> <div className='unit'>Mbps</div> <span className='exchange-icon fa-fw fa-stack'> <i className='far fa-circle fa-stack-2x' /> <i className='fas fa-exchange-alt fa-stack-1x' /> </span> <div className='unit'>MB/s</div> </div> ); } function CardFooter ({ inputValue }) { // 目前就會遇到 Component 分開後無法讀取變數的問題 // 用 props(properties) 可以把要傳送的資料寫在叫用 Component 時(就像使用一般的 HTML attribute 一樣!!) // const inputValue = props.inputValue; // console.log(props) let status; if (inputValue < 15) { status = { description: 'SLOW', backgroundColor: 'red' } } else if (inputValue < 40) { status = { description: 'GOOD', backgroundColor: 'green' } } else if (inputValue >= 40) { status = { description: 'FAST', backgroundColor: '#13d569' } } return ( <div className='card-footer' style={{ backgroundColor: status.backgroundColor }} >{status.description}</div> ); } function CardComponent () { const [ inputValue, setInputValue ] = useState(0); function changeHandler (event) { console.log('onChange!'); // 道理和 VanillaJS 相同 // => 表單的值有改變就會觸發 change // => 事件本身有 event 物件,所以透過觸發函式藉 Argument 得到該物件 // 傳統的寫法 // const value = event.target.value; // 用解構的方式寫 const { value } = event.target; setInputValue(value); } return ( <div className='container'> // Header 部分的 Component <CardHeader /> <div className='card-body'> // UnitControl 部分的 Component <CardUnitControl /> <div className='converter'> <div className='flex-1'> <div className='converter-title'>Set</div> <input type='number' className='input-number' min='0' onChange={changeHandler} /> </div> <span className='angle-icon fa-2x' style={{ marginTop: 30, }} > <i className='fas fa-angle-right' /> </span> <div className='text-right flex-1'> <div className='converter-title'>Show</div> <input className='input-number text-right' type='text' value={inputValue / 8} disabled /> </div> </div> </div> // Footer 部分的 Component <CardFooter inputValue={inputValue}/> </div> ); } ReactDOM.render( <CardComponent />, document.querySelector('#root') ); ``` ## 分割後的 Component 可以透過 `props(properties)` 接到其他 Component 的資料 其實寫在一起也是可以取得資料(就是一個很長很長很長的函式)。[從 Hooks 開始,讓你的網頁 React 起來系列 - Day 10(驗證不分開變數是可以讀取的)](https://codepen.io/lun0223/pen/ZEJNJPz?editors=0010) 切分成不同的 Component,就可以靠著函式的特性(因為 Function Component 就是函式!)利用參數(Paramert)及引數(Arguments)的方式達到資料的傳遞。 ### 什麼是 `props` 呢? `props` 就是物件組成單位 `properties` 的簡稱,`property` 又是由一個 `key` 及 一個 `value` 所組成。 ```javascript= // { property } { myName: 'Lun' } // myName => Key // 'Lun' => Value ``` ### 那該如何在 React 中達到運用 `props` 在分割的小 Component 中傳遞資料呢? **要記得 React 的 Function Component 就是「函式」所以會有函式的特性。** 依照下面的範例可以看出: 1. `CardFooter` 接受一個參數,所以在叫用創造 EC 時就會保存引數。 - 所以在函式中 `inputValue` 的位置就是叫用函式時的引數 - `<ComponentName />` 其實就會叫用 ComponentName 函式 ```javascript= // 在 CardFooter 中寫 console.log() 測試 function CardFooter ({ inputValue }) { console.log('Hello from CardFooter'); // other stuff... } function CardComponent () { // other stuff... return ( <div className='container'> // other stuff... // Footer 部分的 Component <CardFooter /> </div> ); } ReactDOM.render( <CardComponent />, document.querySelector('#root') ); ``` ![](https://i.imgur.com/xeIz37p.png) 2. 現在就要解決如何在叫用 `inputValue` 傳入引數 - 結果十分簡單,就在 `<CardFooter />` 中寫入 `props`(需要自己定義 `keyName` 及 `value`) - 就會變成這樣 `<CardFooter inputValue={inputValue}/>` - 也可以設定多個 `props` ```javascript= const { useState } = React; function CardHeader () { // other stuff... } function CardUnitControl () { // other stuff... } function CardFooter ({ inputValue, testing }) { console.log('Hello From CardFooter'); console.log(inputValue, testing); // 目前就會遇到 Component 分開後無法讀取變數的問題 // 用 props(properties) 可以把要傳送的資料寫在叫用 Component 時(就像使用一般的 HTML attribute 一樣!!) // const inputValue = props.inputValue; // console.log(props) let status; if (inputValue < 15) { status = { description: 'SLOW', backgroundColor: 'red' } } else if (inputValue < 40) { status = { description: 'GOOD', backgroundColor: 'green' } } else if (inputValue >= 40) { status = { description: 'FAST', backgroundColor: '#13d569' } } return ( <div className='card-footer' style={{ backgroundColor: status.backgroundColor }} >{status.description}</div> ); } function CardComponent () { const [ inputValue, setInputValue ] = useState(0); function changeHandler (event) { console.log('onChange!'); // 道理和 VanillaJS 相同 // => 表單的值有改變就會觸發 change // => 事件本身有 event 物件,所以透過觸發函式藉 Argument 得到該物件 // 傳統的寫法 // const value = event.target.value; // 用解構的方式寫 const { value } = event.target; setInputValue(value); } return ( <div className='container'> // other stuff... // Footer 部分的 Component // 設定多個 props // 可以想成 CardFooter({inputValue: inputValue, testing: '測試的'}); <CardFooter inputValue={inputValue} testing={'測試的'}/> </div> ); } ReactDOM.render( <CardComponent />, document.querySelector('#root') ); ``` ![](https://i.imgur.com/GNaFe5l.png) ### 主要資料要留在主 Component 中 在分割 Component 的時候就要記得「主要的資料/狀態」要寫在主 Component 而不是分割出去,因為分割出去就沒有辦法傳遞了。 **因為 Function Scope!** ```javascript= const { useState } = React; function CardHeader () { return ( <div className='card-header'>Network Speed Converter</div> ); } function CardUnitControl () { // other stuff... } function CardConverter () { const [ inputValue, setInputValue ] = useState(0); function changeHandler (event) { const { value } = event.target; setInputValue(value); } return ( // other stuff... ); } function CardFooter () { // other stuff... } function CardComponent () { return ( <div className='container'> <CardHeader /> <div className='card-body'> <CardUnitControl /> <CardConverter /> </div> <CardFooter inputValue={inputValue}/> // Uncaught ReferenceError: inputValue is not defined // 因為在 CardComponent 並沒有辦法找到 CardConverter 中的 inputValue </div> ); } ReactDOM.render( <CardComponent />, document.querySelector('#root') ); ``` ## 如果忘記的話就速看這 之後如果忘記的話可以看 React 文件提供的[範例](https://codepen.io/lun0223/pen/yLoWQgX?editors=0010)。 ## 參考資料 1. [從 Hooks 開始,讓你的網頁 React 起來 7 - 12](https://ithelp.ithome.com.tw/users/20103315/ironman/2668) 2. [Components and Props](https://reactjs.org/docs/components-and-props.html)