(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
我也想要在function component中使用state,可是function不能夠用extend繼承,怎麼辦?
在以前,你就只能乖乖的用class component。在React Ver.16.8後,React hook出現了,它提供一系列讓你能在function component中使用「在class component常用的功能」的方法,useState
就是其中之一。
你可以把useState當成是在function component中state和setState的集合體。它的使用方式是:
變數型態 [state變數名稱, setState函式名稱] = useState(state變數初始值)
/* 舉例來說 */
const [percent, changePercent] = useState("20%");
在上面的例子,我們宣告了一個變數和一個函式,第一個percent
是變數,變成了等同於在class component中的this.state.percent
;而第二個changePercent
變成了等同於(值)=>{this.setState({percent:值})}
的函式(這裡的函式在語法上沒有限定命名原則,但一般會以set做為開頭,這篇是為了配合前一篇的程式碼才用change當開頭)。當我們在function component中使用:
changePercent("70%");
這時percent就會變成70%
。
實際上useState
只是一個函式,它會接收一作為初始值的參數並回傳一個包含兩個值的array,第一個值是state、第二個值是用來對剛那個state做setState的函式。
console.log( useState("初始值"))
而我們所做的const [percent, changePercent]=
,是利用javascript的解構賦值,也就是我們一次宣告兩個變數,percent會變成等號後面賦予的array的第一個值,changePercent會變成等號後面賦予的array的第二個值。
在之前,我們有一個App.js
:
import React, { Component } from 'react';
class App extends Component{
constructor(props) {
super(props);
this.state={
percent:"30%"
}
this.changePercent=this.changePercent.bind(this);
}
changePercent(){
this.setState({percent:"70%"})
}
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,height:"100%",borderRadius:"10px"}}></div>
</div>
<button onClick={this.changePercent}>轉換百分比 </button>
</div>
);
}
}
export default App;
現在,我們來新增一個Progress.js
,讓他有跟剛剛App.js
一模一樣的功能,但是是用function component實現:
引入React以及useState
import React, { useState } from 'react';
建立Progress function,並把剛剛App.js
的render()
部份貼過來
const Progress=()=>{
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,height:"100%",borderRadius:"10px"}}></div>
</div>
<button onClick={this.changePercent}>轉換百分比 </button>
</div>
);
}
用useState在function中建立我們要的state變數以及改變它的方法
const Progress=()=>{
const [percent, changePercent] = useState("30%");
return(
(略)
)
}
把在return()
中使用原本class的state以及function的地方,改成使用剛剛建立的percent
以及函數
<div className="progress-bar" style={{backgroundColor:"#fe5196",width:percent,height:"100%",borderRadius:"10px"}}></div>
<button onClick={()=>{changePercent("70%")}}>轉換百分比 </button>
最後,記得export後在index.js中使用
import React, { useState } from 'react';
const Progress=()=>{
const [percent, changePercent] = useState("20%");
console.log( useState("初始值"))
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,height:"100%",borderRadius:"10px"}}></div>
</div>
<button onClick={()=>{changePercent("70%")}}> 轉換百分比 </button>
</div>
);
}
export default Progress;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Progress from './Progress'
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<div>
<Progress/>
</div>,
document.getElementById('root')
);
serviceWorker.unregister();
這樣就以function完成了一個和上一篇一模一樣的component。
精確的說法是你不能在這些地方去定義產生React hook。(例如,宣告由變數和函式並從useState取得)
為什麼會這樣呢? 原因是和React hook的運作原理有關。
剛剛提到,我們只是用變數去接useState提供給我們的state和setState函式。對useState而言,它是依照順序去分辨每一個hook,而不是用我們的定義內容/來接的變數的名字去分辨。
這是第一次render發生的事:
// ------------
// 你做的事情
// ------------
useState(0)
useState('哈哈哈')
useState(true)
// -------------
// 對React hook而言
// -------------
我產生了一個hook,它是第1個,製造一個state並把其值指定為0
我產生了一個hook,它是第2個,製造一個state並把其值指定為'哈哈哈'
我產生了一個hook,它是第3個,製造一個state並把其值指定為true
// ...
re-render時:
// ------------
// 你做的事情
// ------------
useState(0)
useState('哈哈哈')
useState(true)
// -------------
// 對React hook而言
// -------------
去找第1個hook -> 找到useState(0)
去找第2個hook -> 找到useState('哈哈哈')
去找第3個hook -> 找到useState(true)
// ...
而迴圈、if-else、nest function有可能在某次re-render時不會執行。,以剛剛為例,如果今天沒有任何防護措施,假設今天把第2個React hook放在某個if,當某次re-render時這個if的執行條件為false,就會發生:
這是第一次render發生的事:
// ------------
// 你做的事情
// ------------
useState(0)
useState('哈哈哈')
useState(true)
// -------------
// 對React hook而言
// -------------
我產生了一個hook,它是第1個,製造一個state並把其值指定為0
我產生了一個hook,它是第2個,製造一個state並把其值指定為'哈哈哈'
我產生了一個hook,它是第3個,製造一個state並把其值指定為true
// ...
re-render時(當沒有執行useState('哈哈哈')
):
// ------------
// 你做的事情(這一次 useState('哈哈哈') 並沒有被執行)
// ------------
useState(0)
useState(true)
// -------------
// 對React hook而言
// -------------
去找第1個hook -> 找到useState(0)
去找第2個hook -> 找到useState('哈哈哈')
// ...
這樣會取得錯誤的hook。因此,React所選擇的防護措施是不讓你產生在這些scope中定義的React hook。以確保每次render時的順序一模一樣。
其中一個原因是元件結構會變簡單。
以前,當我們的專案到一定的規模後,component越來越多,JSX夾雜各處。有的時候即使只是很簡單的元件,卻因為要follow class component的語法而變得看起來雜亂、一大堆巢狀結構。於是,工程師開始思考能不能在結構相對陽春的function component中,使用class component常用的功能。
於是,React hook就出現了。
現在,請你比較一下App.js
和Progress.js
。功能完全一模一樣的元件,前者用了二十幾行,後者用了十幾行。
從這次鐵人賽不少以React hook為主體的系列文就可以知道,目前像這樣更簡易的元件設計方式正在被React社群大力推行。你就知道工程師看code看到多爆氣了
總之,多用React hook吧!