iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 22
0
Modern Web

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

【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫

到目前屬於教學的部分也有約21篇了。如果你是從第一篇一路跟下來,相信看到這邊的時候:你應該也忘記的差不多了XD

所以,我們會用兩篇來實作一個元件,藉此複習一下前面教過的東西。然後第24篇時我們會繼續拿這個元件來講別的事。

那我們要做什麼呢

我們要做一個像是這個樣子的動態進度條:
image alt

目標:

index.js
|____ App.js
      |____ProgressDIY.js
  1. 我們先以class component製作ProgressDIY.js。
  2. 在App.js使用我們的進度條元件(ProgressDIY.js)。
  3. 用App.js的value放在props,來控制內進度條的比例(長度)。
  4. ProgressDIY.js要能顯示目前進度,並且放兩個按鍵在旁邊。
  5. 按第一個按鍵(增加到90)時,改變App.js的value,讓此進度條從目前的進度以動畫方式逐漸改變至90%。
  6. 按第二個按鍵(減少到10)時,改變App.js的value,讓此進度條從目前的進度以動畫方式逐漸改變至10%。
  7. 即使還沒有跑到前一個目標進度,當目標進度被更新時,進度條不能停下,必須直接往新的目標長度更動。
  8. 外進度條的色碼為rgba(0,0,0,0.2),長度為200px,圓角為10px,高度為7px。
  9. 內進度條色碼為:#fe5196,長度、圓角和外進度條相同。

可以先自己試試看。下面是我的作法:

Step 1 - 準備好App.js:

我們先在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

Step 2 - 做好ProgressDIY.js的基本架構

我們先把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來解決這件事。

Step3 - 加入recursive來實現漸變

為什麼你不直接用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);
    });  
  }

補上其他不足的地方

目前還剩下四個地方還沒有完成

  1. 在constructor中補上percent,以及綁定increase()decrease()

    constructor(props) {
        super(props);
        this.state={ 
            percent:0,
        }
        this.increase=this.increase.bind(this);
        this.decrease=this.decrease.bind(this);
      }
    
  2. 在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();
         }
       }
    
  3. 元件建立時,把value的值指定給percent。有兩種做法:

    static getDerivedStateFromProps(props, state){
        return {percent:props.value};
    }
    

    或是

    componentDidMount(){
        this.setState({percent:this.props.value})
    }
    

    建議使用componentDidMount,原因是有時候我們傳給元件的值是在這邊呼叫fetch去取得。

  4. 把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的程式碼。


上一篇
【React.js入門 - 21】 各階層Component的溝通
下一篇
【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫
系列文
給初入JS框架新手的React.js入門31

尚未有邦友留言

立即登入留言