(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
到目前屬於教學的部分也有約21篇了。如果你是從第一篇一路跟下來,相信看到這邊的時候:你應該也忘記的差不多了XD
所以,我們會用兩篇來實作一個元件,藉此複習一下前面教過的東西。然後第24篇時我們會繼續拿這個元件來講別的事。
我們要做一個像是這個樣子的動態進度條:
目標:
index.js
|____ App.js
      |____ProgressDIY.js
rgba(0,0,0,0.2),長度為200px,圓角為10px,高度為7px。#fe5196,長度、圓角和外進度條相同。可以先自己試試看。下面是我的作法:
我們先在App.js做好用來控制的state和改變他的function。由於結構比較簡單,這邊用function component和useState就搞定。
import React,{useState} from 'react';
import ProgressDIY from './ProgressDIY'
const App=()=>{
    const [value,setValue]=useState(10);
    return(
      <ProgressDIY value={value} onClick={(e)=>{setValue(e.target.value)}}/>
    );
}
export default App;
注意onClick那邊因為等等是要接收button上的value,所以要以e.target.value為參數丟入setValue。
我們先把class component的基本架構弄出來
import React, { Component } from 'react';
class ProgressDIY extends Component{
  constructor(props) {
    super(props);
  }
    render(){
        return(
        );
    }
}
export default ProgressDIY;
然後在render()中弄出進度條跟按鍵的元素,這邊就是純粹看對於html、css的掌握了:
    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".toString()+"%",height:"100%",borderRadius:"10px"}}></div>
            </div>
            目前比率: {this.state.percent.toFixed(0)}%
            <button>增加比率至90</button>
            <button>減少比率至10</button>
        </div>
        );
    }
接著,我們先用props中的value綁定寬度。並且為了要在子元件修改自己的props,把用來更改props中的value的函式(在剛剛App.js中我們提供的是setValue並綁在onClick上)及對應目標要修改的value值綁在按鍵上:
   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.props.value.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
           </div>
           目前比率: {this.props.value.toFixed(0)}%
           <button value={90} onClick={this.props.onClick}>增加比率至90</button>
           <button value={10} onClick={this.props.onClick}>減少比率至10</button>
       </div>
       );
   }
到這裡,如果你執行就會發現,進度條雖然有正確的變換比例,卻不是漸變,而是直接變成目標比例:
所以接下來,我們就要用recursive+state來解決這件事。
為什麼你不直接用style的transition就好了
主要原因是當使用別人寫的插件時,很多時候只能使用給定的介面,並不能用css/style屬性去直接操控它。
所以我們就來寫recursive(遞迴)函式吧!
以由短到長變動為例:用一個state來當作用來控制目前的寬度的參數,定義每一次都把該state增加1的函式,並且在每次加完後和props上的value做比較。如果還沒達到,就用setTimeout去設定在短時間內再次呼叫此函式,如果達到,就結束整個流程;然而有一個例外case是如果在漸變到一半時突然換目標數字,可能會有某幾個瞬間同時存在上升函數和下降函數的情形。所以這邊要先把setTimeout存起來,晚點如果突然改變目標,就能用clearTimeout把剛剛的setTimeout移除。在這個範例中,我用percent作為用來控制寬度比例的state。
以上的想法畫成流程圖是這個樣子的:

實際化為程式碼:
  increase() {
    const percent = this.state.percent + 1;
    this.setState({ percent },()=>{
      if (this.state.percent >= this.props.value) {
        return;
      }
      this.tm = setTimeout(this.increase, 20);
    });  
  }
注意要去存setTimeout(this.increase, 20),等等突然變換時才能夠把它清除。
比較特別的是,因為setState是需要一點時間的,我們必須在setState的第二個參數函式中做接下來的事,否則如果在外面用this.state.percent去比較,if可能會拿到更新前的percent。
另外在這邊使用了this.setState({ percent }),這樣的寫法setState會自動去把和「function scope的percent變數」同名的state指定為它(percent)的值。
由長變短的邏輯和由短變長差不多,只是第一步的加號變減號,第二步的>變成<。
直接來看實際的程式碼:
  decrease() {
    const percent = this.state.percent - 1;
    this.setState({ percent },()=>{
      if (this.state.percent <= this.props.value) {
        return;
      }
      this.tmTwo = setTimeout(this.decrease, 20);
    });  
  }
目前還剩下四個地方還沒有完成
在constructor中補上percent,以及綁定increase()和decrease()。
constructor(props) {
    super(props);
    this.state={ 
        percent:0,
    }
    this.increase=this.increase.bind(this);
    this.decrease=this.decrease.bind(this);
  }
在props的value被改變時觸發increase()或decrease(),讓percent開始變動。當前面相反方向漸變的setTimeout還在做時,要用clearTimeout去清除,避免重複執行。注意這邊不需要有===時的case,函式自己會結束。
   componentDidUpdate(prevProps, prevState){
     if(prevProps.value>this.props.value){
       if(this.tm)
           clearTimeout(this.tm);
         this.decrease();
     }
     else if(prevProps.value<this.props.value){
       if(this.tmTwo)
         clearTimeout(this.tmTwo);
       this.increase();
     }
   }
元件建立時,把value的值指定給percent。有兩種做法:
static getDerivedStateFromProps(props, state){
    return {percent:props.value};
}
或是
componentDidMount(){
    this.setState({percent:this.props.value})
}
建議使用componentDidMount,原因是有時候我們傳給元件的值是在這邊呼叫fetch去取得。
把render中的寬度和進度顯示改成以percent控制
    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.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
            </div>
            目前比率: {this.state.percent}%
            <button value={90} onClick={this.props.onClick}>增加比率至90</button>
            <button value={10} onClick={this.props.onClick}>減少比率至10</button>
        </div>
        );
    }
這樣就完成了所有的事情。
import React, { Component } from 'react';
class ProgressDIY extends Component{
  constructor(props) {
    super(props);
    this.state={ 
        percent:0,
    }
    this.increase=this.increase.bind(this);
    this.decrease=this.decrease.bind(this);
  }
  increase() {
    const percent = this.state.percent + 1;
    this.setState({ percent },()=>{
      if (this.state.percent >= this.props.value) {
        return;
      }
      this.tm = setTimeout(this.increase, 20);
    });  
  }
  decrease() {
    const percent = this.state.percent - 1;
    this.setState({ percent },()=>{
      if (this.state.percent <= this.props.value) {
        return;
      }
      this.tmTwo = setTimeout(this.decrease, 20);
    });  
  }
  componentDidMount(){
        this.setState({percent:this.props.value})
  }
  componentDidUpdate(prevProps, prevState){
    if(prevProps.value>this.props.value){
        if(this.tm)
          clearTimeout(this.tm);
        this.decrease();
    }
    else if(prevProps.value<this.props.value){
      if(this.tmTwo)
        clearTimeout(this.tmTwo);
      this.increase();
    }
  }
    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.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
            </div>
            目前比率: {this.state.percent}%
            <button value={90} onClick={this.props.onClick}>增加比率至90</button>
            <button value={10} onClick={this.props.onClick}>減少比率至10</button>
        </div>
        );
    }
}
export default ProgressDIY;
這是自己某個專案中使用的元件,因為注意到它一次包含props、state、setState、生命週期、元件之間溝通...等重要觀念,所以才拿來當作這次的練習主題。
另外,我們也可以利用「任何props、state被改變時componentDidUpdate就會被執行」的特性,直接在componentDidUpdate中做recursive,在這個範例會讓程式碼變得更簡潔。
在下一篇,我們來把這個元件全部改成function component,同時我也會附上class component改在componentDidUpdate中做recursive的程式碼。