iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
0
Modern Web

給初入JS框架新手的React.js入門系列 第 12

【React.js入門 - 12】 state 與 詳解setState語法

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


在前一章,我們發現當變數name被改變時,我們的按鍵並沒有被更新。這是因為React 的 virtual DOM 只有在兩個東西被改變時才會更新,第一個是先前提過的props,第二個則是state

state是class component的預設好會去檢查的一個特殊的member data。就如同上述所言,當state被改變時,會進入re-render的update程序,更新畫面。

接下來我們會用state,來設定並改變以下這個進度條的寬度:

import React, { Component } from 'react';
class App extends Component{
  constructor(props) {
    super(props);
  }

    render(){
        return(
          <div>
            <div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
              <div className="progress-bar" style={{backgroundColor:"#fe5196",width:"10%",height:"100%",borderRadius:"10px"}}></div>
            </div>
          </div>
        );
    }
}
export default App;

宣告與初始化

state的結構也是一個物件,建立的方法很簡單,在建構子裡宣告即可:

  constructor(props) {
    super(props);
    this.state={ 變數名稱A: 初始化值, 變數名稱B: 初始化值, (...類推)};
  }
  
  /* 排版習慣為 */
  
  constructor(props) {
    super(props);
    this.state={ 
        變數名稱A: 初始化值, 
        變數名稱B: 初始化值, 
        (...類推)
    };
  }

讀取

讀取的方式和props一樣,也就是:

this.state.變數名稱

例如在下面的程式碼(App.js)中,我們用percent這個state去控制progress-bar中style的width:

import React, { Component } from 'react';
class App extends Component{
  constructor(props) {
    super(props);
    this.state={ // 宣告state物件,內包含一個變數percent
      percent:"30%" 
    }
  }

    render(){
        return(
          <div>
            <div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
              <div className="progress-bar" style={{backgroundColor:"#fe5196",width:this.state.percent,height:"100%",borderRadius:"10px"}}></div>
            </div>
          </div>
        );
    }
}
export default App;

執行結果:

修改

state這個變數是read-only的,我們並不能用this.state.變數=值直接修改state,而是必須要透過React預寫好的函式setState()來更改。(註:跟使用其他函式一樣,前面要加上this)
例如:

  changePercent(){
    this.setState({ percent:"70%" })
  }

state是一個物件,所以丟入setstate()中的也必須是一個物件(也就是範例中的{ percent:"70%" })。

接續上面的範例,我們用changePercent去修改剛剛progress-bar的寬度

import React, { Component } from 'react';
class App extends Component{
  constructor(props) {
    super(props);
    this.state={
      percent:"30%"
    }
    this.changePercent=this.changePercent.bind(this); //綁定changePercent
  }

  changePercent(){ //加入changePercent函式
    this.setState({percent:"70%"})
  }


    render(){
        return(
          <div>
            <div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
              <div className="progress-bar" style={{backgroundColor:"#fe5196",width:this.state.percent,height:"100%",borderRadius:"10px"}}></div>
            </div>
          </div>
        );
    }
}
export default App;

然後加入一個按鍵去觸發changePercent:

<button onClick={this.changePercent}>轉換百分比 </button>

所有的程式碼:

import React, { Component } from 'react';
class App extends Component{
  constructor(props) {
    super(props);
    this.state={
      percent:"30%"
    }
    this.changePercent=this.changePercent.bind(this); //綁定changePercent
  }

  changePercent(){ //加入changePercent函式
    this.setState({percent:"70%"})
  }


    render(){
        return(
          <div>
            <div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
              <div className="progress-bar" style={{backgroundColor:"#fe5196",width:this.state.percent,height:"100%",borderRadius:"10px"}}></div>
            </div>
            <button onClick={this.changePercent}>轉換百分比 </button>
          </div>
        );
    }
}
export default App;

執行之後,當按下按鍵,粉紅色的部分就會變成寬占滿70%:

(我不是gif,所以不會動)

setState中「存在但沒有被寫到的state」不會被移除,不存在的state會被建立。

假設目前state為

this.state={
    percent: 20,
    mounted: false
}

當我們使用this.setState({percent: 70})時,mounted並不會從state中被移除。

比較需要說明的是「不存在的state會被建立」,這句話的意思是我們不一定要在constructor中建立state/所有state。當setState被呼叫時,如果它發現有member data不屬於目前的state,就會自動建立它;如果在constructor連state的宣告都沒寫,就會自動建立state。
例如在這個狀態下:

this.state={
    percent: 20,
    mounted: false
}

以下的函式就代表在改變percent為40的同時,創造一個叫做counter的state

this.setState({percent:40,counter:0});

新的state:

this.state={
    percent: 40,
    mounted: false,
    counter:0
}

用setState移除state

前面提過setState中「存在但沒有被寫到的state」不會被移除,那移除其中一個state要怎麼做呢?答案很簡單,把該state指定為undefined就可以:

this.setState({mounted: undefined});

這樣mounted就會不存在於我們的state。

使用變數設定state值時,可以只寫this.setState({變數名稱})

當寫成this.setState({變數名稱})時,setState會自動去找state中有沒有和該變數同名字的member data。如果有,就會把它設定為變數值。如果沒有,就會自動在state中建立同名字的member data。

let counter=5;
this.setState({counter});
/* 如果目前的state有counter,把它指定為5。如果沒有,則創造一個叫counter的state */

對於state中的物件不能只修改單一屬性

要特別注意的是,如果在state裡面宣告物件,修改時並不能夠單獨修改物件的單一屬性
例如:

this.state={
    styleData:{
        width: "30%",
        height: "50%"
    }
}

如果我們使用

this.setState({ styleData:{width:"70%"} });

state裡面的styleData並不會保留height屬性,而是直接變成只有width:"70%"的物件。如果想要只更改state內的物件的單一屬性並保留其他屬性,可以這樣寫:

this.setState({ 
    styleData:{
        width: "70%",
        height: this.state.styleData.height
    } 
});

setState不會馬上做完

setState通常需要花一點細微的時間,雖然不會察覺,但是由於這點加上js的非同步特性,在setState後面用到state的函式常常會拿到改變前的state值。

這個時候可以搭配setState預設的第二個參數。(感謝 神Q超人 大大的留言補充,自己寫這系列的同時也在參考他的在 React 生態圈內打滾的一年 feat. TypeScript )

setState的第二個參數可有可無,它是個function,當state被設定完之後,就會執行。我們可以利用這個參數來作想在state改變後的事情:

this.state={
    percent: 20
}
this.setState({percent: 70},()=>{
        console.log(this.state.percent);// 這樣會印出70
    }
)

其實setState第一個參數也是函式

回頭去查了一下原始官方文件,setState的第一個參數也是一個函式,只是因為當我們給予的是物件而不是函式時,會自動讓物件淺層合併至新的 state 中,所以我們才能直接寫this.setState(物件)
完整的setState應該長這樣:

this.setState((state, props) => {
     /* 第一個參數函式 */
  return {新的state};
},()=>{
     /* 第二個參數函式,state改變後執行 */
});


小結:

終於把前面一直提過的state講完了,另外雖然這裡提了相當多有關setState的語法,不過使用時建議還是統一以免造成同事困擾XD

下一篇會來講現在火紅的React hook中的useState。


上一篇
【React.js入門 - 11】 開始進入class component
下一篇
【React.js入門 - 13】 useState - 在function component用state
系列文
給初入JS框架新手的React.js入門31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

1
神Q超人
iT邦研究生 5 級 ‧ 2019-09-23 14:40:22

setState() 可以有兩個參數ㄛ,如果要取得改變後的值可以在第二個 Function 中作 callback

this.setState({ name: 'hi' },
  () => {
    // 這裡可以取得改變後的 state
    console.log(this.state);
  }
);

這樣子就不用使用 Promise 了~

Andy Chang iT邦研究生 4 級 ‧ 2019-09-23 14:44:18 檢舉

啊啊好,自己不知道這個用法
等等會趕快補上,感謝大大指點 orz

0
Yves Wang
iT邦新手 5 級 ‧ 2022-06-28 16:59:31

謝謝大大分享,說明的真的很清楚~~~~

我要留言

立即登入留言