iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 23
2
Modern Web

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

【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫

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


在這篇,我們要以function component製作和【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫相同的元件。

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

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)時,此進度條會從目前的進度以動畫方式逐漸改變至90%。
  6. 按第二個按鍵(減少到10)時,此進度條會從目前的進度以動畫方式逐漸改變至90%。
  7. 即使還沒有跑到前一個目標進度,當目標進度被更新時,進度條不能停下,必須直接往新的目標長度更動。
  8. 外進度條的色碼為rgba(0,0,0,0.2),長度為200px,圓角為10px,高度為7px。
  9. 內進度條色碼為:#fe5196,長度、圓角和外進度條相同。

這個是把上一篇的recursive改在componentDidUpdate中做的程式碼:

import React, { Component } from 'react';
class ProgressDIY extends Component{
  constructor(props) {
    super(props);
    this.state={ 
        percent:0,
    }
  }

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


  componentDidUpdate(prevProps, prevState){
    if((prevProps.value!==this.props.value)||(prevState.percent!=this.state.percent)){
        if(this.state.percent>this.props.value){
          if(this.tm)  
            clearTimeout(this.tm)
          this.tmtwo=setTimeout(()=>{this.setState({percent:this.state.percent-1})}, 20)
        }
        else if(this.state.percent<this.props.value){
          if(this.tmTwo)  
            clearTimeout(this.tmTwo)
          this.tm=setTimeout(()=>{this.setState({percent:this.state.percent+1})}, 20)
        }
    }
  }

    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;

Step 1 : 做好ProgressDIY.js的基本架構

以函式為架構宣告,記得參數要有propsexport也要記得寫。

const ProgressDIY=(props)=>{

}
export default ProgressDIY;

Step 2 : 引入useState、useEffect、useRef,不用引入component

因為我們不希望一開始就漸變,所以要分隔出第一次渲染前和第一次渲染後,必須多引入useRef。因為function component不會需要繼承React.Component,所以就不引入。

import React, { useState,useEffect,useRef} from 'react';

Step 3 : 用useState建立控制目前的寬度的state和setState函式

percent當作用來控制目前的寬度的參數。

const [percent,setPercent]=useState(10);

Step 4 : 在return()中弄出進度條跟按鍵的元素

如果你是用class component去改,this.state.percent要變成percentthis.props.名稱要變成props.名稱

  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:percent.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
      </div>
      目前比率: {percent}%
      <button value={90} onClick={props.onClick}>增加比率至90</button>
      <button value={10} onClick={props.onClick}>減少比率至10</button>
    </div>
  )

在Step 5 開始之前: 思考該有多少useEffect

在開始加入useEffect前,我們要思考的事情是:

我目前要實現的事情中,有哪些事情是依賴「監控相同參數的變動」? 是不是第一次要執行?

共三件事情要實踐:

  1. 第一次渲染時的初始值設定 (只有第一次要執行)
  2. 由長變短漸變。(依賴props.value和percent,但第一次不能執行)
  3. 由短變長漸變。(依賴props.value和percent,但第一次不能執行)

也就是我們2和3一定可以放在同一個effect中。又因為任何effect第一次都會執行,所以只要想辦法在這個effect中分隔出「第一渲染、非第一次渲染」,就能把這三件事都放在同一個effect中。

Step 5: 在useEffect中,搭配useRef去紀錄是否為第一次渲染,讓第一次和非第一次渲染可以做不同的事。

這邊有兩點要注意:

  1. 要用之前提過配合useRef的方法去分隔出在mount後才開始運作的componentDidUpdate
  2. 第二個參數的監控值除了props.value外,因為要做recursive,也要監控percent
  const mounted=useRef();
  useEffect(()=>{
    if(!mounted.current){ //componentDidMount

      mounted.current=true;
    }
    else{  //componentDidUpdate

    }
  },[props.value,percent]);

Step 6: 在第一次渲染的定義區中完成初始化

這邊要用定義的set函式,並且由於props是從函式參數取得,初始值要去掉this

  const mounted=useRef();
  
  useEffect(()=>{
    if(!mounted.current){ //componentDidMount
      setPercent(props.value);
      mounted.current=true;
    }
    else{ //componentDidUpdate


    }

  },[props.value,percent]);

Step 7: 創建用來存setTimeout的變數

和剛剛紀錄是否mount的變數一樣,這裡也是要用useRef去創造用來存setTimeout的變數。

  const mounted=useRef();
  const tm=useRef();
  const tmTwo=useRef();
  
  useEffect(()=>{
    if(!mounted.current){ //componentDidMount
      setPercent(props.value);
      mounted.current=true;
    }
    else{ //componentDidUpdate

    }

  },[props.value,percent]);

Step 8: 在非第一次渲染的定義區域中完成recursive

這裡的運作邏輯跟我們在最一開始提供的「用componentDidUpdate」做recursive一模一樣:

用一個state來當作用來控制目前的寬度的參數,定義每一次都把該state增加/減少1的函式,並且在每次加完後和props上的value做比較。如果還沒達到,就用setTimeout去設定在短時間內再次呼叫此函式;這邊要先把setTimeout存起來,晚點就能用clearTimeout把剛剛的setTimeout移除,再結束整個流程。這樣可以避免如果在漸變到一半時突然換目標數字,會有某幾個瞬間同時存在上升函數和下降函數的情形。


轉化為程式碼:

  const mounted=useRef();
  const tm=useRef();
  const tmTwo=useRef();
  
  useEffect(()=>{
    if(!mounted.current){//componentDidMount
      setPercent(props.value);
      mounted.current=true;
    }
    else{ //componentDidUpdate
      if(percent>props.value){
        if(tm.current)
          clearTimeout(tm.current)
        tmTwo.current=setTimeout(()=>{setPercent(percent-1)},20);
      }
      else if(percent<props.value){
        if(tmTwo.current)
          clearTimeout(tmTwo.current)
        tm.current=setTimeout(()=>{setPercent(percent+1)},20);
      }
    }

  },[props.value,percent]);

所有的程式碼:

import React, { useState,useEffect,useRef} from 'react';
const ProgressDIY=(props)=>{
  const [percent,setPercent]=useState(10);
  
  const mounted=useRef();
  const tm=useRef();
  const tmTwo=useRef();
  
  useEffect(()=>{ 
    if(!mounted.current){//componentDidMount
      setPercent(props.value);
      mounted.current=true;
    }
    else{ //componentDidUpdate
      if(percent>props.value){
        if(tm.current)
          clearTimeout(tm.current)
        tmTwo.current=setTimeout(()=>{setPercent(percent-1)},20);
      }
      else if(percent<props.value){
        if(tmTwo.current)
          clearTimeout(tmTwo.current)
        tm.current=setTimeout(()=>{setPercent(percent+1)},20);
      }
    }

  },[props.value,percent]);



  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:percent.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
      </div>
      目前比率: {percent}%
      <button value={90} onClick={props.onClick}>增加比率至90</button>
      <button value={10} onClick={props.onClick}>減少比率至10</button>
    </div>
  )
}

export default ProgressDIY;

小結:

在React hook推出後,React開發社群開始推廣function component,並降低對於class component的依賴。而使用React hook的思維邏輯有些地方會跟class component不太一樣,所以我在Step 5之前特別去講設計架構時的想法。這次鐵人賽中,很多React系列都是以function component為主題,主要也是因為希望讀者能不被class component的設計邏輯侷限。

在這一篇之後,我們也會盡量用function component來實現所有的東西。

本篇範例中,class component和function component的程式碼長度其實差不多,而下一篇,我們將會用custom hook去讓程式碼更簡潔。


上一篇
【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫
下一篇
【React.js入門 - 24】 Custom hook - 給我另一個超推React hook的理由
系列文
給初入JS框架新手的React.js入門31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言