(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的程式碼。