(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在這篇,我們要以function component製作和【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫相同的元件。
我們要做一個像是這個樣子的動態進度條:
index.js
|____ App.js
|____ProgressDIY.js
目標:
- 我們先以class component製作ProgressDIY.js。
- 在App.js使用我們的進度條元件(ProgressDIY.js)。
- 用App.js的value放在props,來控制內進度條的比例(長度)。
- ProgressDIY.js要能顯示目前進度,並且放兩個按鍵在旁邊。
- 按第一個按鍵(增加到90)時,此進度條會從目前的進度以動畫方式逐漸改變至90%。
- 按第二個按鍵(減少到10)時,此進度條會從目前的進度以動畫方式逐漸改變至90%。
- 即使還沒有跑到前一個目標進度,當目標進度被更新時,進度條不能停下,必須直接往新的目標長度更動。
- 外進度條的色碼為
rgba(0,0,0,0.2)
,長度為200px,圓角為10px,高度為7px。- 內進度條色碼為:
#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;
以函式為架構宣告,記得參數要有props
,export
也要記得寫。
const ProgressDIY=(props)=>{
}
export default ProgressDIY;
因為我們不希望一開始就漸變,所以要分隔出第一次渲染前和第一次渲染後,必須多引入useRef。因為function component不會需要繼承React.Component,所以就不引入。
import React, { useState,useEffect,useRef} from 'react';
用percent
當作用來控制目前的寬度的參數。
const [percent,setPercent]=useState(10);
如果你是用class component去改,this.state.percent
要變成percent
,this.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>
)
在開始加入useEffect前,我們要思考的事情是:
我目前要實現的事情中,有哪些事情是依賴「監控相同參數的變動」? 是不是第一次要執行?
共三件事情要實踐:
也就是我們2和3一定可以放在同一個effect中。又因為任何effect第一次都會執行,所以只要想辦法在這個effect中分隔出「第一渲染、非第一次渲染」,就能把這三件事都放在同一個effect中。
這邊有兩點要注意:
useRef
的方法去分隔出在mount後才開始運作的componentDidUpdateprops.value
外,因為要做recursive,也要監控percent
。 const mounted=useRef();
useEffect(()=>{
if(!mounted.current){ //componentDidMount
mounted.current=true;
}
else{ //componentDidUpdate
}
},[props.value,percent]);
這邊要用定義的set函式,並且由於props是從函式參數取得,初始值要去掉this
。
const mounted=useRef();
useEffect(()=>{
if(!mounted.current){ //componentDidMount
setPercent(props.value);
mounted.current=true;
}
else{ //componentDidUpdate
}
},[props.value,percent]);
和剛剛紀錄是否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]);
這裡的運作邏輯跟我們在最一開始提供的「用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去讓程式碼更簡潔。