### More on React.js ![](https://i.imgur.com/eg5ECEc.png) Spring 2019 。 Ric Huang --- ### Recall: Virtual DOM * React Only Updates What’s Necessary * Try this and watch it on cosole: ```jsx function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render(element, document.getElementById('root')); } setInterval(tick, 1000); ``` ---- ### Recall: Use "this.props" to define component's properties * Define the component and its properties ```jsx class Caption extends Component { render() { return <caption> {this.props.name}'s Score </caption>; } } ``` * Instantiate the component ```jsx <Caption name={someJSExpression} /> ``` ---- ### Note: "this.props" are read-only * You cannot assign or change values to **this.props** * What can we do if we want to change the React components dynamically according to some I/O events? --- ### "this.state" in React Component * 如果想要在 component 裡頭 “記住” 一些資訊(e.g. 按讚數量, 目前計算結果...),用來增加網頁的互動,則需要用 “state” ![](https://i.imgur.com/QHp9rfr.png) ---- ### Understanding "state" * 要了解 React component 裡頭的 "state",我們要同時學三件事情: 1. this.state 2. Component lifecycle 3. Event handling ---- ### "this.state" * 語法 ```jsx class MyClass extends React.Component { constructor(props) { super(props); this.state = { // as an "object" declaration statePropName: statePropValue, ... }; } } ``` e.g. ```jsx class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; // initialize } } ``` ---- ### Referred by "this.state..." ```jsx class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render(<Clock / >, document.getElementById('root')); ``` 但... clock 不會動了... (fixed later) ---- * 當然,我們可以試著這樣做... ```jsx class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } setInterval( () => ReactDOM.render(<Clock / >, document.getElementById('root')), 1000); ``` 但這是不 work 的... Virtual DOM 並不會知道 state 被 update 而需要更新畫面 ---- ### Note: "state" is private to the class. You cannot pass in value to it. ```jsx class MyClass extends React.Component { constructor(props) { super(props); this.state = { count: 0 } } render() { return ( <div> {this.props.name} is called {this.state.count} times! </div> ); } } ReactDOM.render(<MyClass name="Ric" count={8} / >, document.getElementById('root')); ``` Output: "Ric is called 0 times!" // No error, not '8' ---- ### So, "this.state" is initialized in constructor. How do we update it? ### Or say, once the React component is rendered on the screen, how do we re-render it? --- ### To get a better understanding on how React state works, we should look at "component lifecycle" first! ---- ### Component Lifecycle * 一個 React component 從一開始被定義、render 到螢幕、因為 "某些因素" 讓 virtual DOM 察覺到 component 的內容改變而 re-render 畫面、到最後這個 component 從 DOM 被拔掉,這個 component 總過會經歷過三個階段的 lifecycle --- 1. **Mounting**: component 即將被生成並且插入到 DOM 上面 (i.e. displayed on page) 2. **Updating**: 因為 props or state 的改變而 trigger virtual DOM 去更新畫面 3. **Unmounting**: component 即將從 DOM 被移除 ---- ### To be more specific... * Mounting — component 即將被生成並且插入到 DOM 上面 (i.e. displayed on page) * constructor() * componentWillMount() * render() * componentDidMount() ---- ### To be more specific... * Updating — 因為 props or state 的改變而 trigger virtual DOM 去更新畫面 * componentWillReceiveProps() * shouldComponentUpdate() * componentWillUpdate() * render() * componentDidUpdate() // 為什麼沒有 "componentWillReceiveState()" ---- ### To be more specific... * Unmounting — component 即將從 DOM 被移除 * componentWillUnmount() ---- ### Why should I care about the component lifecycle? * 利用 "Mounting" 階段中的 methods 初始化 props/state 的值,並且做一些必要的設定 * 透過 event handling 更新 states 的值 (why not "props"?),並且在 updating 階段中 re-render component * (如有必要) 當 component 要被從 DOM 移除之時,在 unmounting 階段把一些資源回給系統 ---- ### In the previous clock tick example... ```jsx class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; } componentDidMount() { setInterval(() => this.tick(), 1000); } tick() { this.setState({ date: new Date() }); } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render(<Clock / >, document.getElementById('root')); ``` ---- * 在 component render 完畢之後設定 "this.tick()" 每秒會被呼叫一次 ```jsx componentDidMount() { setInterval(() => this.tick(), 1000); } ``` * 更新 state 的 value ```jsx tick() { this.setState({ date: new Date() }); } ``` * Note: "ReactDOM.render()" 不必重新被呼叫!!更新畫面是 virtual DOM 的事,你只要更新 state value 就好了! ---- ### Note: Use "setState()" to update state!! * 用 setState() 才會去通知 virtual DOM 重新呼叫 render() 來更新畫面 * 如果這樣寫: ```jsx tick() { this.state.date = new Date(); } ``` // 不會有 error message, 但 clock 不會動! ---- ### Release system resources * 在前面的範例中,component 的更新是藉由 system clock tick, 所以如果 component 因故被從 DOM 拔掉,即使畫面不再顯示這個 component,它的 tick() 還是會被一直呼叫。 * 可以宣告一個變數把產生的 timer ID 存下來,然後再 unmounting phase 把它停掉 ```jsx componentDidMount() { this.timerID = setInterval(() => this.tick(), 1000); } componentWillUnmount() { clearInterval(this.timerID); } ``` --- ### Using React Event Handler * 在前面的範例中,component 的更新是藉由 system clock tick, 但在一般的應用當中,常常是由 I/O events 來觸發畫面的更新 * Since React is just a JS library, 它的 event handling 基本上跟 JS 差不多... ---- ### Recall: Event handling in HTML/JS 1. addEventListener() ```javascript var targetElement = document.getElementById("target"); targetElement.addEventListener("click", function() {...}); ``` 2. GlobalEventHandlers ```javascript let log = document.getElementById('log'); log.onclick = inputChange; function inputChange(e) {...} ``` 3. As a tag attribute ```javascript <div class="myClass" onclick="clickHandler()"> function clickHandler() {...} ``` ---- * In React, we can usually do: ```jsx class MyButton extends React.Component { handleClick = () => { console.log('this is:', this); } render() { return ( <button onClick={this.handleClick}> // 也可以寫成:<button onClick={()=>this.handleClick()}> Click me </button> ); } } ReactDOM.render(<MyButton / >, document.getElementById('root')); ``` --- ### In-class Practice * 用 React 實作一個可以 加/減 的計數器 ![](https://i.imgur.com/7R90lK5.png) * 初始值:100, 按 '+' 則 +1, 按 '-' 則 -1 * 至少 create 一個 class Counter ---- ### 先不要翻到下一頁!!!! ---- ### 防雷頁 ---- ```jsx class Counter extends Component { constructor(props) { super(props); this.state = { count: 100 }; } handleInc = () => this.setState(state => ({ count: state.count + 1 })); handleDec = () => this.setState(state => ({ count: state.count - 1 })); render() { return ( <div> <h1>{this.state.count}</h1> <span> <button onClick={this.handleInc}>+</button> <button onClick={this.handleDec}>-</button> </span> </div> ); } } ``` ---- ### Split the code into smaller components * 在上述的例子,為了讓程式碼更模組化,以利未來的擴增,試著把 '+', '-' buttons 也變成 class Button * 像這樣: ```jsx <span> <Button text="+" onClick=...></Button> <Button text="-" onClick=...></Button> </span> ``` ---- ### class Button 怎麼寫? ---- ```jsx class Button extends Component { render() { return <button onClick={this.props.onClick}>{this.props.text} </button>; } } ``` ```jsx // In class Counter <span> <Button text="+" onClick={this.handleInc} /> <Button text="-" onClick={this.handleDec} /> </span> ``` ---- ### 不過,有沒有覺得 class Button 這樣寫有點冗? ---- ### Functional (Dummy) Components * 在前面的例子,整個畫面的主要邏輯都是寫在 Counter 裡面,而 Button 的角色只是個 component,也沒有自己的 state. 因此,建議可以將這兩種 components 分開,放在底下兩個子目錄: 1. Containers: 如:Counter, 存著 state/props 以及一些主要的邏輯 2. Components: 如:Button, 沒有自己的 states, 也沒有什麼複雜的邏輯,建議改寫成 Functional Component ---- ### Button as a Functional Component ```jsx // In Components/Button.js import React from 'react' export default ({ onClick, text }) => { return <button onClick={onClick}>{text}</button>; } ``` ```jsx // In Containers/App.js import React, { Component } from 'react' import Button from '../components/Button' ... ``` --- ### 關於 state 一些注意事項 ([ref](https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly)) ---- #### 1. Do Not Modify State Directly ```jsx // Wrong: this won't rerender the component this.state.comment = 'Hello'; // Correct: use "setState()" this.setState({comment: 'Hello'}); ``` * The only place where you can assign this.state is the constructor. ---- #### 2. State Updates May Be Asynchronous * React may batch multiple setState() calls into a single update for performance. * Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state. ```jsx // Wrong: state 的 value 可能沒有被 update 到 this.setState({ counter: this.state.counter + this.props.increment, }); // Correct: 這樣會拿 previous state 的值來 update this.setState((state, props) => ({ counter: state.counter + props.increment })); ``` ---- #### 3. State Updates are Merged * 你可以針對 state object 裡頭不同的 properties 分開來 update ```jsx componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); } ``` * 上述兩個 update 可以被獨立呼叫,不會互相影響 ---- #### 4. Data Flows Down * State is local and encapsulated. 任何一個 component 不會知道它的 parent or child components 是 stateful or stateless. 它也無法去讀取它 parent or child component states 的值 * 就像 Button 無法去讀 Counter 的值 * 所以,我們通常會把 state 往上 (in terms of DOM structure) 提,然後如果 child component 的 view 會 depend on parent component state 的值,則在 child component 宣告的地方把 parent state 的某個 prop 傳給 child props. ```jsx // In some parent component's function <ChildComponent someProp={this.state.someProp} / > ``` --- ### Typechecking With PropTypes * 由於 JS 本身是弱型別的關係,browser 不時會很雞婆的幫你做型別的轉換,雖然有時這樣做會讓人覺得很方便,但也常常會因此而造成一些奇怪,很難抓出來的 bugs, 因此,在某些地方強制規定型別會有助於讓不合型別規定的地方提早噴出 error 出來,讓你比較容易 debug. ---- ### Typechecking With PropTypes ```jsx // 語法 class MyClass extends Component { ... this.props.someProp1... ... this.props.someProp2... } MyClass.propTypes = { // 注意 'p' 的大小寫 someProp1: PropTypes.string, someProp2: PropTypes.func }; ``` ---- ### Examples of PropTypes * 基本的 JS types ```jsx optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalSymbol: PropTypes.symbol ``` * Anything that can be rendered or an React element ```jsx optionalNode: PropTypes.node, optionalElement: PropTypes.element ``` ---- ### Other Examples of PropTypes ```jsx // an instance of a class optionalMessage: PropTypes.instanceOf(Message), // some value in an enum optionalEnum: PropTypes.oneOf(['News', 'Photos']), // one of the types optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), // Add '.isRequired' to make sure a warning // is shown if the prop isn't provided. requiredFunc: PropTypes.func.isRequired, requiredAny: PropTypes.any.isRequired, // '.element.isRequired' 用來指定只能有一個 child node children: PropTypes.element.isRequired ``` ---- ### Default Prop Values * 用 **defaultProps** 來指定 default property value ```jsx class Greeting extends React.Component { render() { return (<h1>Hello, {this.props.name}</h1>); } } // Specifies the default values for props: Greeting.defaultProps = { name: 'Stranger' }; // Renders "Hello, Stranger": ReactDOM.render( <Greeting />, document.getElementById('example') ); ``` --- ### Thinking in React ([ref](https://reactjs.org/docs/thinking-in-react.html)) ![](https://i.imgur.com/YmtX50c.png) ---- * 假設我們透過 JSON API 收到底下的資料,而產生如上頁的產品價格表 ```json [ {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"}, {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"}, {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"}, {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"}, {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"}, {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"} ]; ``` ---- #### Step 1: Break The UI Into A Component Hierarchy ![](https://i.imgur.com/956o2kD.png) <small> * FilterableProductTable (orange) * SearchBar (blue) * ProductTable (green) * ProductCategoryRow (turquoise) * ProductRow (red) </small> ---- #### Step 2: Build A Static Version in React * 只用 props 來實現一個靜態網頁 <== 沒有互動 * Top-down thinking and data flow ```jsx // The top class class FilterableProductTable extends React.Component { render() { return ( <div> <SearchBar /> <ProductTable products={this.props.products} /> </div> ); } } let PRODUCTS = [ ... ]; // that JSON table ReactDOM.render( <FilterableProductTable products={PRODUCTS} />, document.getElementById('container') ); ``` ---- #### Step 3: Identify The Minimal (but complete) Representation Of UI State * React component with "states" => stateful =>元件狀態/資料與之前的狀態/資料(i.e.歷史)有關 * Think: 可否用最少的states來導出所有需要的資訊? ``` 1. Is it passed in from a parent via props? If so, it probably isn’t state. 2. Does it remain unchanged over time? If so, it probably isn’t state. 3. Can you compute it based on any other state or props in your component? If so, it isn’t state. ``` * 前面的例子,states 為: ``` 1. The search text the user has entered 2. The value of the checkbox ``` ---- #### Step 4: Identify Where Your State Should Live * 原則:One-way data flow (top-down), 所以 state 要盡量往上提到「需要看到該資訊的所有 components 的 (lowest) common root component」,讓所有需要該資訊的 components 可以透過 state 的往下傳而得到資訊。 ```jsx class FilterableProductTable extends React.Component { ... render() { return { ... <SearchBar filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} /> }}} ``` <small> * In case such common root doestn't exist, we can create a wrapper component just to hold the state information </small> ---- #### Step 5: Add Inverse Data Flow * Think: "this.state.filterText" 以及 "this.state.inStockOnly" 存在 FilterableProductTable, 透過 state --> props 去傳給底下的 SearchBar 以及 ProductTable. 但問題是,filterText 以及 inStockOnly 這兩個資訊是在 SearchBar 得到,那他們要怎麼「寫回」上面 FilterableProductTable 的 state 呢? ==> 利用 event handler 以及 callback functions! ---- * React 官方連結的那個[例子](https://reactjs.org/docs/thinking-in-react.html#step-5-add-inverse-data-flow)實在是太不必要的複雜了... 我們把前面的 in-class practice 加一個 text input 試試看: ![](https://i.imgur.com/7xQrY3r.png) * '+' to +1, '-' to -1, input box to specify a number ---- ### Create a functional component for Input ```jsx import React from 'react'; export default ({onKeyPress}) => { return <input type="text" placeholder="Enter a number..." onKeyPress={onKeyPress} />; } ``` ---- ### In "Counter.js" ```jsx class Counter extends Component { ... setNumber = num => this.setState(() => ({ count: num })); handleInput = e => { if (e.key === "Enter") { const value = parseInt(e.target.value); if (value === 0 || value) this.setNumber(value); e.target.value = ""; e.target.blur(); } }; render() { return ( ... <Input onKeyPress={this.handleInput} /> ); } } ``` ---- * Note: ```jsx <Input onKeyPress={this.handleInput} /> // Can be rewritten as: <Input onKeyPress={ e => this.handleInput(e)} /> ``` And in ```jsx handleInput = e => { if (e.key === "Enter") ... ``` * The 'e' above is called "[SyntheticEvent](https://reactjs.org/docs/events.html)". They are cross-browser wrappers around the browser’s native events. --- ### Practices in this spring break #### (Practice04 & Practice05) ---- * Directory structures * 先在 **WebProg2019** 底下開個 **Practice04** and **Practice05** 目錄,然後再各自底下用 create-react-app 產生 **"own"** project 目錄 * 手動產生一個空目錄 **review** * HTML 的 entry 在 **public/index.html**, 然後 JaveScript 的在 **src/index.js**。把主要邏輯操作的 components 放在 **src/containers** 目錄,然後把一些沒有什麼邏輯,主要只是 render 畫面的 components 放在 **src/components** 目錄下面。 * Make sure 在 own 底下執行 **npm start** 可以開啟你的 project,並且記得把 **node_modules** 加到 **.gitignore**, 不要上傳到 github. ---- ### Practice04: TODO List in React * 就是把你 Homework01 的 TODO list 用 React 重寫一遍哦! * 練習重點: * 跟著 "[Thinking in React](https://reactjs.org/docs/thinking-in-react.html)" 思考一遍,create 適當的 classes。 * What are the states? * How do you add inverse data flow? * Practice Due: 9pm, 03/31 (Sunday) * Review Due: 9pm, 04/02 (Tuesday) ---- ### Practice05: Calculator in React * 請下載 starter's src from [here](https://github.com/ric2k1/ric2k1.github.io/blob/master/W6_0327/practice/calculator-starter/src.tgz) * Copy to your **Practice05/own**, decompress it to replace the original **src** * Implement under **src/{containers,components}**. * **npm start** to run, and **npm test** for auto testing * 利用提供的 CSS 做出來的 Calculator 長這樣:[here](https://camo.githubusercontent.com/87ea4a738b93c32bc6a6c50c941b5a1d7163ffea/68747470733a2f2f7075752e73682f78336963582f323630303431643439342e706e67) * 請注意 '0' 那格是兩倍大,請選用適當的 class 以利 npm test * Practice Due: 9pm, 04/07 (Sunday) * Review Due: 9pm, 04/09 (Tuesday) ---- ### Practice05: 功能要求說明 * 進階功能(可以不用做) — 正負號(+/-)、百分比(%)、小數點(.) 這三個按鍵只需要有 UI,不需要功能 * 重置計算機 — AC (all clear) 按鍵,需要可以把計算狀態 (state) 全清空 * 數字按鍵 — 會跟一般計算機一樣按了會累積之後要當運算元的數字。例如依序按下 6, 1, 9, 8,會產生 6198 來當作運算元 * 因為不實作小數當作運算元,所以第一個數字按零可以忽略 ---- ### Practice05: 功能要求說明 * 運算子按鍵 (+, -, x, ÷, =) * 如果前面已經有 pending 的運算則先把前面的運算求值: * 按下 3, +, 6:這時候畫面還是 6 * 但再按(+, -, x, ÷, =)任何一個就會更新運算結果 * 如果前面也是運算子,新的運算子會把舊的換掉 * 按下 3, +, -, 1, =:這時候結果為 2,+ 會被 - 換掉 * 如果前面是運算元,則先 pending 存至 state --- ### That's it! ### See you in 2 weeks!
{"metaMigratedAt":"2023-06-14T20:45:57.629Z","metaMigratedFrom":"YAML","title":"More on React.js (03/27)","breaks":true,"slideOptions":"{\"theme\":\"beige\",\"transition\":\"fade\",\"slidenumber\":true}","contributors":"[{\"id\":\"752a44cb-2596-4186-8de2-038ab32eec6b\",\"add\":22933,\"del\":3882},{\"id\":\"1616274c-795e-4491-b5fb-f94b3a9a2335\",\"add\":37,\"del\":37},{\"id\":\"a202b02b-072b-4474-be14-c5a3f8932dbb\",\"add\":37,\"del\":0}]"}
    2319 views