# React 中的 state, props ###### tags: `React` ## State > 一個組件的顯示形態是可以由它數據狀態和配置參數決定的,一個組件可以擁有自己的狀態,就像一個點贊按鈕,可以有“已點贊”和“未點贊”狀態,並且可以在這兩種狀態之間進行切換。React.js的`state`就是用來存儲這種可變化的狀態的。 在物件導向裡面, 若是要更改state, 則每一個component建立的時候都會呼叫一個function, 叫做constructor. 同時因為繼承因此呼叫時會call`super()` 先來理解下程式碼: ```javascript= class App extends React.Component { constructor(){ super() //這個this是在oop中,所以是指instance this.state = { title:'hello', counter: 1 } } render(){ return ( <div> <h1 onClick={()=>{ this.setState({ counter: this.state.counter + 1 }) }}>{this.state.title}//可以把它看成物件要call值</h1> <div>{this.state.counter}</div> </div> ) } } ``` * 任何要改變`state`的動作都要加`this.setState`, 裡面傳入一個`object` (所以要有key和) ```javascript= this.setState({ counter: this.state.counter + 1 }) ``` 而不是 ```javascript= counter: this.state.counter ++ ``` ### 優化 這一段`onClick`事件, 可以進行優化, 用function包住 ```javascript= ()=> { this.setState ( {counter: this.state.counter + 1} ) } ``` ```javascript= handleClick(){ this.setState({ counter: this.state.counter + 1 }) } ``` 在`render()`裡面,也要修改成`this.handleClick` ```javascript= render(){ return ( <div> <h1 onClick={this.handleClick}>{this.state.title}</h1> <div>{this.state.counter}</div> </div> ) } ``` ### setState補充 這個範例,透過更改`state`實作出簡單的開關, `isLiked`存放在實例的`state`對象當中,這個對像在構造函數里面初始化。這個組件的render函數內,會根據組件的`state`的中的isLiked不同顯示“取消”或“點贊”內容。並且給`button`加上了點擊的事件監聽。 ```javascript= class Title extends React.Component { constructor(){ super() this.state={ isLiked: false } this.button = this.button.bind(this) } button(){ console.log(this.state.isLiked) this.setState({ isLiked: !this.state.isLiked }) console.log(this.state.isLiked) } render(){ return( <h1 onClick={this.button}> {this.state.isLiked? 'good':'bad'} </h1> ) } } class App extends React.Component { render() { const name = 'yoyoyo' return ( <div> <Title /> </div> ) } } ``` > 在`Button`事件監聽函數里面,大家可以留意到,我們調用了`setState`函數,每次點擊都會更新`isLiked`屬性為`!isLiked`,這樣就可以做到點贊和取消功能。 > `setState`方法由父類Component所提供。當我們調用這個函數的時候,React.js會更新組件的狀態`state`,並且重新調用`render`方法,然後再把`render`方法所渲染的最新的內容顯示到頁面上。 注意,當我們要改變組件的狀態的時候,不能直接用`this.state = xxx` 這種方式來修改,如果這樣做React.js就沒辦法知道你修改了組件的狀態,它也就沒有辦法更新頁面。所以,一定要使用React.js提供的`setState`方法,它接受一個物件或者函數作為參數。 ### setState 接受函數參數 這裡還有要注意的是,當你調用`setState`的時候,React.js並不會馬上修改`state`。而是把這個對象放到一個更新隊列裡面,稍後才會從隊列當中把新的狀態提取出來合併到state當中,然後再觸發組件更新。這一點要好好注意。可以體會一下下面的代碼: 你會發現兩次打印的都是false,即使我們中間已經`setState`過一次了。這並不是什麼bug,只是React.js的`setState`把你的傳進來的狀態緩存起來,稍後才會幫你更新到`state`上,所以你獲取到的還是原來的`isLiked`。 所以如果你想在setState之後使用`新的state`來做後續運算就做不到了,例如: ```javascript= Button () { this.setState({ count: 0 }) // => this.state.count 还是 undefined this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN this.setState({ count: this.state.count + 2}) // => NaN + 2 = NaN } ``` 上面的代碼的運行結果並不能達到我們的預期,我們希望`count`運行結果是`3`,可是最後得到的是`NaN`。但是這種後續操作依賴前一個`setState`的結果的情況並不罕見。 這裡就自然地引出了`setState`的第二種使用方式,可以==接受一個函數作為參數==。React.js會把上一個`setState`的結果傳入這個函數,你就可以使用該結果進行運算、操作,然後返回一個對像作為更新`state`的對象: ```javascript= Button () { this.setState((prevState) => { return { count: 0 } }) this.setState((prevState) => { return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1 }) this.setState((prevState) => { return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3 }) // 最后的结果是 this.state.count 为 3 } ``` ### setState 合併 上面我們進行了三次`setState`,但是實際上組件只會重新渲染一次,而不是三次;這是因為在React.js內部會把JavaScript事件循環中的消息隊列的同一個消息中的`setState`都進行合併以後再重新渲染組件。 ## props 看官網學到一課 ```javascript= class HelloMessage extends React.Component{ render(){ return( <div> Hello {this.props.name} </div> ) } } // Hello Taylor ReactDOM.render(<HelloMessage name='Taylor'/>, document.getElementById('root')) ``` > 就是傳進來給你的, Component之間可以透過props把值傳下去 我預期`this.state.counter`會變得更複雜而不是從1開始. 所以把原本的地方=> 換成`component`型式 但是問題來了, 我原本寫的好好的, `this.state.counter` 怎麼傳進去 很簡單, 在裡面在定義一個物件名稱`number`,把值傳到裡面即可 ```javascript= class Counter extends React.Component{ render(){ return( <div>{this.props.number}</div> ) } } class App extends React.Component { constructor(){ super() this.state = { title: 'React', counter : 1 } this.handleCounter = this.handleCounter.bind(this) } handleCounter(){ this.setState({ counter: this.state.counter + 1 }) } render() { return ( <div> <h1 onClick={this.handleCounter}>{this.state.title}</h1> <Counter number={this.state.counter}/> </div> ) } } ``` ### 運作流程 1. 進入`<App />` => render`handleCounter`和`this.state.title` 2. 看到`<Counter />` call `Counter Component` => render`number` 10/4 更新, 我現在是在`App`裡面,但是我又進入了`counter`,在`counter`中 我想取到`App`的`state`, 但是我在不同的`component`裡面, 所以這時候我要先定義一個變數,去接`this.state.number`, 然後在該==新組建上去調用父元素的參數來配置該組件== ### props.children 或是可以改成針對`title`分割, 注意我在`App`裡面是用`<title />`包住,所以在接收時, 要用`this.props.children` 而這樣做的好處是, 可以再包住的`Counter`裡面任意加東西 ```javascript= class Counter extends React.Component{ render(){ return( <div>{this.props.children}</div> ) } } class Title extends React.Component { render() { return ( <h1>{this.props.children}</h1> ) } } class App extends React.Component { constructor(){ super() this.state = { title: 'React', counter : 1 } this.handleCounter = this.handleCounter.bind(this) } handleCounter(){ this.setState({ counter: this.state.counter + 1 }) } render() { return ( <div> <h1 onClick={this.handleCounter}>{this.state.title}</h1> <Title> <a href="https://google.com">{'hello2'}</a> </Title> <Counter> {this.state.counter} </Counter> </div> ) } } ``` ## Class Component vs functional Component 當這個componet不用`state`,不用繼承,就可以用functional Component, 簡單return東西出來. ```javascript= function Counter(props){ return( <div>{props.number}</div> ) } //等同於 function Counter(props){ const {number} = props return( <div>{number}</div> ) } //等同於 => 常見做法 function Counter({ number }){ return( <div>{number}</div> ) } // => function裡面的參數已經定義為props,所以這樣寫是指props = {number}(解構語法) //ES6 const Counter =(props)=>{ return( <div>{props.number}</div> ) } ``` ## Component 間的溝通 已經在`APP`裡面定義了`state`, `title`和`counter` 兩個屬性. `return`完後, 因為還想再讓他分割成`component` 所以, 所以又用獨立出`component` 示意: `App component` => return => `title component` => 透過props接收 =>新`title component` => 想要在裡面增加功能 在children時,沒有辦法去改變parent的state, 所以不能把功能下在`新title component`, 把功能下在上一層`title component` 比較合理. ### 整個流程 1. render Title text = 1, handleCounter = funcA 2. trigger Title > h1 > onClick => props.handelCounter => funcA 3. App.handleCounter => setState => counter: 2 4. re-render 5. render Title text = 2 , handleCounter = funcA 掌握一個流程, 只要改變`state` 就會re-render ```javascript= function Counter({ number }){ return( <div>{number}</div> ) } const Title = (props) => { return ( <h1 onClick={props.handleCounter}>{props.text}</h1> ) } class App extends React.Component { constructor(){ super() this.state = { title: 'React', counter : 1 } this.handleCounter = this.handleCounter.bind(this) } handleCounter(){ this.setState({ counter: this.state.counter + 1 }) } render() { console.log('render') return ( <div> <Title handleCounter = {this.handleCounter} text={this.state.counter}/> {/* <h1 onClick={this.handleCounter}>{this.state.title}</h1> */} <Counter number={this.state.counter}/> </div> ) } } ``` ## props透過父元素來傳送參數 已經在changeCounter定義了`number`這個參數,但是沒有給值 所以在子元素的`Text`來定義他的值. ==因為要執行這個function而不是簡單取值,所以寫成執行的語句== ```javascript= const Text =(props)=>{ return( <div> <h1 onClick={()=>{ props.changeCounter(Math.random()) }}>{props.counters}</h1> </div> ) } class Title extends React.Component { constructor(){ super() this.state = { counter: 1 } this.Button = this.Button.bind(this) this.changeCounter = this.changeCounter.bind(this) } Button(){ this.setState({ counter: this.state.counter + 1 }) } changeCounter(number){ this.setState({ counter: number }) } render(){ return( <div> <h1>{this.state.counter}</h1> <Text changeCounter={this.changeCounter} counters={this.state.counter}/> </div> ) } } ``` ==只有`Component`自己可以改變`state`,外部要call只能透過props來取值,真的要改變只能去呼叫父元素提供出的function==