useState
是一個(gè)Hook函數(shù),讓你在函數(shù)組件中擁有state變量可缚。它接收一個(gè)初始化的state霎迫,返回是一個(gè)數(shù)組,數(shù)組里有兩個(gè)元素帘靡,第一個(gè)元素是當(dāng)前狀態(tài)值和另一個(gè)更新該值的方法知给。
本教程主要是針對(duì)于React中的useState
做一個(gè)詳細(xì)的描述,它等同于函數(shù)組件中的this.state
/this.setState
描姚,我們將會(huì)圍繞下面的問(wèn)題逐一解析:
- React中的類組件和函數(shù)組件
- React.useState hook做了什么
- 在React中聲明狀態(tài)
- React Hooks: Update State
- 在useState hook中使用對(duì)象作為狀態(tài)變量
- React Hooks中如何更新嵌套對(duì)象狀態(tài)
- 多個(gè)狀態(tài)變量還是一個(gè)狀態(tài)對(duì)象
- 使用useState的規(guī)則
- useReducer Hook的使用
如果您是剛開始學(xué)習(xí)使用useState涩赢,請(qǐng)查看官方文檔或者該視頻教程。
React中的類組件和函數(shù)組件
React有兩種類型的組件:類組件和函數(shù)組件轩勘。
類組件是繼承React.Component
的ES6類筒扒,它有自己的狀態(tài)和生命周期函數(shù):
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
message: ""
};
}
componentDidMount() {
/*...*/
}
render() {
return <div>{this.state.message}</div>
}
}
函數(shù)組件是一個(gè)函數(shù),它能接收任何組件的屬性作為參數(shù)绊寻,并且可以返回有效的JSX花墩。
function Message(props) {
return <div>{props.message} </div>
}
//或者使用箭頭函數(shù)
const Message = (props) => <div>{props.message}</div>
正如所看到的,函數(shù)組件沒(méi)有任何狀態(tài)和生命周期方法澄步。不過(guò)冰蘑,在React16.8,我們可以使用Hooks村缸。
React Hooks是方法祠肥,它可以給函數(shù)組件添加狀態(tài)變量,并且可以模擬類組件的生命周期方法梯皿。他們傾向以use作為Hook名的開始仇箱。
React.useState hook做了什么
正如之前了解的,useState
可以給函數(shù)組件添加狀態(tài)东羹,函數(shù)組件中的useState
可以生成一系列與其組件相關(guān)聯(lián)的狀態(tài)剂桥。
類組件中的狀態(tài)總是一個(gè)對(duì)象,不過(guò)Hooks中的狀態(tài)可以是任意類型百姓。每個(gè)state可以有單一的值,也可以是一個(gè)對(duì)象况木、數(shù)組垒拢、布爾值或者能想到的任意類型。
So火惊,你會(huì)什么時(shí)候用useState
Hook呢求类?它對(duì)組件自身的狀態(tài)很有用,然而大項(xiàng)目可能會(huì)需要另外的狀態(tài)管理方案屹耐。
React聲明狀態(tài)
useState
是React
的命名輸出出尸疆,因此你可以這么寫:
React.useState
或者可以直接這么寫:
import React, { useState } from "react";
然而不像在類組件里聲明狀態(tài)對(duì)象那樣,useState
允許聲明多個(gè)狀態(tài)變量:
import React from "react";
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "",
list: "",
}
}
/*...*/
}
useState
Hook一次只能聲明一個(gè)狀態(tài)變量,不過(guò)這個(gè)狀態(tài)變量可以是任意類型的:
import React, { useState } from "react";
const Message = () => {
const messageState = useState("");
const listState = useState([]);
}
useState
接收狀態(tài)的初始值作為一個(gè)參數(shù)寿弱。
正如之前例子展示的犯眠,可以直接給函數(shù)傳遞,也可以使用函數(shù)來(lái)延遲初始化該變量(當(dāng)初始化狀態(tài)基于一次昂貴的計(jì)算症革,這種方式是很有用的):
const Message = () => {
const messageState = useState(() => expensiveComputation());
/*...*/
}
初始化的值僅僅會(huì)在第一次渲染時(shí)被賦值(如果他是一個(gè)函數(shù)筐咧,也是會(huì)在初次渲染時(shí)執(zhí)行)。
在后續(xù)的更新中(由于組件本身的狀態(tài)更改或者是說(shuō)父組件導(dǎo)致的變化)噪矛,useState
Hook參數(shù)(初始值)將會(huì)被忽略量蕊,當(dāng)前的值將會(huì)被使用。
理解它是非常重要的艇挨,舉個(gè)例子残炮,如果你想更新基于組件接收的新屬性的狀態(tài):
const Message = (props) => {
const messageState = useState(props.message);
}
只單獨(dú)使用useState
不會(huì)工作的,因?yàn)樗膮?shù)僅僅在第一次生效缩滨,并不是每次屬性更改時(shí)生效(可以結(jié)合useEffect
使用势就,具體查看該回答)
不過(guò),useState
不是像之前所說(shuō)的僅僅返回一個(gè)變量楷怒。
它返回的是一個(gè)數(shù)組蛋勺,第一個(gè)元素是狀態(tài)變量,第二個(gè)元素是更新該變量值的方法鸠删。
const Message= () => {
const messageState = useState("");
const message = messageState[0]; // 是一個(gè)空字符串
const setMessage = messageState[1]; // 是一個(gè)方法
}
一般我們會(huì)選擇數(shù)組解構(gòu)的方式來(lái)簡(jiǎn)化上述代碼:
const Message = () => {
const [message, setMessage] = useState("");
}
在函數(shù)組件中可以像其他變量一樣使用狀態(tài)變量:
const Message = () => {
const [message, setMessage] = useState("");
return <p>{message}</p>;
}
但是為什么useState
會(huì)返回一個(gè)數(shù)組呢抱完?
因?yàn)榕c對(duì)象相比、數(shù)組是非常靈活且容易使用刃泡。
如果這個(gè)方法返回的是一個(gè)包括一系列屬性集的對(duì)象巧娱,那么就不能很容易自定義變量名,比如:
// 沒(méi)有使用對(duì)象解構(gòu)
const messageState = useState( '' );
const message = messageState.state;
const setMessage = messageState;
//使用對(duì)象解構(gòu)
const { state: message, setState: setMessage } = useState( '' );
const { state: list, setState: setList } = useState( [] );
React Hooks: 更新狀態(tài)
useState
返回的第二個(gè)元素是一個(gè)方法烘贴,它用新值來(lái)更新狀態(tài)變量禁添。
舉個(gè)??,使用輸入框在每次改變時(shí)更新狀態(tài)變量示例:
const Message = () => {
const [message, setMessage] = useState( '' );
return (
<div>
<input
type="text"
value={message}
placeholder="Enter a message"
onChange={e => setMessage(e.target.value)}
/>
<p>
<strong>{message}</strong>
</p>
</div>
);
};
然而桨踪,這個(gè)更新函數(shù)不會(huì)立即更新值老翘。相反它會(huì)排隊(duì)等待更新操作。在重新渲染組件后锻离,useState
的參數(shù)將被忽略铺峭,這個(gè)更新方法將會(huì)返回最新的值。
如果你需要用之前的值來(lái)更新狀態(tài)汽纠,你一定得傳遞一個(gè)接收之前值的方法來(lái)返回新值示例:
const Message = () => {
const [message, setMessage] = useState("");
return (
<div>
<input
type="text"
value={message}
placeholder="Enter a message"
onChange={(e) => {
const val = e.target.value;
setMessage((prev) => prev + val);
}}
/>
<p>
<strong>{message}</strong>
</p>
</div>
);
};
在useState
hook中使用對(duì)象作為狀態(tài)變量
當(dāng)使用對(duì)象時(shí)卫键,需要記住的是:
- 不可變的重要性
useState
返回的更新方法不是像類組件中的setState
合并對(duì)象
關(guān)于第一點(diǎn),如果你是用相同的值作為當(dāng)前值來(lái)更新state(React使用的Object.is
來(lái)做比較)虱朵,React不會(huì)觸發(fā)更新的莉炉。
當(dāng)使用對(duì)象時(shí)钓账,很容易出現(xiàn)下面的錯(cuò)誤示例,輸入框不能輸入文本:
const MessageOne = () => {
const [messageObj, setMessageObj] = useState({ message: "" });
return (
<div>
<input
type="text"
value={messageObj.message}
placeholder="Enter a message"
onChange={(e) => {
messageObj.message = e.target.value;
setMessageObj(messageObj);
}}
/>
<p>
<strong>{messageObj.message}</strong>
</p>
</div>
);
};
上面的例子沒(méi)有創(chuàng)建一個(gè)新對(duì)象絮宁,而是改變已經(jīng)存在的狀態(tài)對(duì)象梆暮。對(duì)于React來(lái)說(shuō),它們是同一個(gè)對(duì)象羞福。為了正常運(yùn)行惕蹄,我們創(chuàng)建一個(gè)新對(duì)象示例:
onChange={(e) => {
const newMessageObj = { message: e.target.value };
setMessageObj(newMessageObj);
}}
這個(gè)讓我們看到了你需要記住的第二件事情。
當(dāng)你創(chuàng)建一個(gè)狀態(tài)變量時(shí)治专,類組件中的this.setState
自動(dòng)合并更新對(duì)象卖陵,而函數(shù)組件中useState
的更新方法則是直接替換對(duì)象 。
繼續(xù)上面的例子张峰,如果我們給message
對(duì)象添加一個(gè)id
屬性泪蔫,將會(huì)發(fā)生什么呢示例:
const MessageThree = () => {
const [messageObj, setMessageObj] = useState({ message: "", id: 1 });
return (
<div>
<input
type="text"
value={messageObj.message}
placeholder="Enter a message"
onChange={(e) => {
const newMessageObj = { message: e.target.value };
setMessageObj(newMessageObj);
}}
/>
<p>
<strong>
{messageObj.id}: {messageObj.message}
</strong>
</p>
</div>
);
};
當(dāng)只更新message
屬性時(shí),React將替換原先的狀態(tài)值{ message: '', id: 1 }
喘批,當(dāng)觸發(fā)onChange
屬性時(shí)撩荣,狀態(tài)值將僅僅包含message
屬性:
{message: "····"} // id屬性丟失了
當(dāng)然,通過(guò)替換的對(duì)象和擴(kuò)展運(yùn)算之前的對(duì)象結(jié)合作為參數(shù)也可以在函數(shù)組件中復(fù)制setState()
的行為示例:
onChange={(e) => {
const val = e.target.value;
setMessageObj((prevState) => {
return { ...prevState, message: val };
});
}}
...prevState
會(huì)得到對(duì)象所有的屬性饶深,message: val
會(huì)重新賦值給message
屬性餐曹。
當(dāng)然使用Object.assign
也會(huì)得到相同的結(jié)果(需要?jiǎng)?chuàng)建新對(duì)象)示例:
//使用Object.assign
setMessageObj((prevState) => {
return Object.assign({}, prevState, { message: val });
});
不過(guò)擴(kuò)展運(yùn)算可以簡(jiǎn)化這個(gè)操作,而且也可以應(yīng)用到數(shù)組上敌厘。
一般來(lái)講台猴,當(dāng)在數(shù)組上使用時(shí),擴(kuò)展運(yùn)算移除了括號(hào)俱两,你可以用舊數(shù)組中的值創(chuàng)建另一個(gè)數(shù)組:
[
...['a', 'b', 'c'],
'd'
]
// Is equivalent to
[ 'a', 'b', 'c','d']
再來(lái)個(gè)??饱狂,如何用數(shù)組來(lái)使用useState
示例:
需要注意的是,處理多維數(shù)組時(shí)需要謹(jǐn)慎的使用擴(kuò)展運(yùn)算宪彩,因?yàn)榭赡茏罱K的結(jié)果不是你所期待的休讳。
所以這個(gè)時(shí)候,我們就需要考慮使用對(duì)象作為狀態(tài)尿孔。
React Hooks中如何更新嵌套對(duì)象狀態(tài)
JS中俊柔,多維數(shù)組是數(shù)組里嵌套數(shù)組:
[
['value1','value2'],
['value3','value4']
]
你可以在用它們來(lái)把你所有的狀態(tài)變量集中在一個(gè)地方,然而活合,為了這個(gè)目的雏婶,最好使用內(nèi)嵌對(duì)象:
{
'row1' : {
'key1' : 'value1',
'key2' : 'value2'
},
'row2' : {
'key3' : 'value3',
'key4' : 'value4'
}
}
當(dāng)使用內(nèi)嵌對(duì)象和多維數(shù)組時(shí),有一個(gè)問(wèn)題是Object.assign
和擴(kuò)展運(yùn)算是創(chuàng)建了一個(gè)淺拷貝并非是深拷貝芜辕。
當(dāng)拷貝數(shù)組時(shí)尚骄,擴(kuò)展運(yùn)算僅僅做了一層的拷貝块差,因此侵续,對(duì)于多維數(shù)組來(lái)說(shuō)倔丈,使用它是不合適的,就像下面例子中所示(使用
Object.assign()
和擴(kuò)展元算結(jié)果都是true):
let a = [[1], [2], [3]];
let b = [...a];
b.shift().shift(); // 1
//此時(shí)數(shù)組a的結(jié)果是[[], [2], [3]]
StackOverflow關(guān)于上面的例子提供了一個(gè)比較好的解釋状蜗,不過(guò)目前重要的是需五,當(dāng)使用內(nèi)嵌對(duì)象時(shí),我們不能用擴(kuò)展運(yùn)算來(lái)更新狀態(tài)對(duì)象轧坎。
再舉個(gè)??
const [messageObj, setMessageObj] = useState({
author: "",
message: {
id: 1,
text: "",
}
});
來(lái)宏邮,先看看更新text
字段的錯(cuò)誤方式示例:
//錯(cuò)誤, 文本更改后,messageObj的值是{author: "", message: {id: 1, text: ""}, text: "*"}
setMessageObj((prevState) => ({
...prevState,
text: val,
}));
//錯(cuò)誤缸血,文本更改后messageObj值是{id: "*", text: "*"}
setMessageObj((prevState) => ({
...prevState.message,
text: val
}));
//錯(cuò)誤蜜氨,文本更改后,messageObj的值是{author: "", message: {text: "*"}}捎泻,缺少id屬性
setMessageObj((prevState) => ({
...prevState,
message: {
text: val,
}
}));
為了能正確的更新text
屬性飒炎,我們需要拷貝一個(gè)原始對(duì)象,這個(gè)新對(duì)象包括整個(gè)原始對(duì)象的所有屬性:
setMessageObj((prevState) => ({
...prevState, //賦值第一層的key值
message: { //創(chuàng)建包含更新key值的對(duì)象
...prevState.message, //復(fù)制包含key值的對(duì)象
text: val, //給需要更新的字段重新賦值
}
}));
以同樣的方式笆豁,我們也可以更新author
字段:
setMessageObj((prevState) => ({
author: "Joe",
message: { ...prevState.message },
}));
如果message
對(duì)象變化了郎汪,則用以下的方式:
setMessageObj((prevState) => ({
author: "Joe",
message: {
...prevState.message,
text: val,
},
}));
聲明多個(gè)狀態(tài)變量還是一個(gè)狀態(tài)對(duì)象
當(dāng)你的應(yīng)用中使用多個(gè)字段或值作為狀態(tài)變量時(shí)蜒滩,你可以選擇組織多個(gè)變量:
const [id, setId] = useState(-1);
const [message, setMessage] = useState('');
const [author, setAuthor] = useState('');
或者使用一個(gè)對(duì)象狀態(tài)變量:
const [messageObj, setMessage] = useState({
id: 1,
message: '',
author: ''
});
不過(guò)班巩,需要謹(jǐn)慎的使用復(fù)雜數(shù)據(jù)結(jié)構(gòu)(內(nèi)嵌對(duì)象)的狀態(tài)對(duì)象豺撑,考慮下這個(gè)?? :
const [messageObj, setMessage] = useState({
input: {
author: {
id: -1,
author: {
fName:'',
lName: ''
}
},
message: {
id: -1,
text: '',
date: new Date(),
}
}
});
如果你需要更新嵌套在對(duì)象深處的指定字段時(shí)今艺,你必須復(fù)制所有其他對(duì)象和包含該指定字段的的對(duì)象的鍵值對(duì)一起復(fù)制:
setMessage(prevState => ({
input: {
...prevState.input,
message: {
...prevState.input.message,
text: '***',
}
}
}));
在某些情況下谓传,拷貝深度內(nèi)嵌對(duì)象是比較昂貴的裆操,因?yàn)镽eact可能會(huì)依賴那些沒(méi)有改變過(guò)的字段值重新渲染你應(yīng)用的部分內(nèi)容窗宇。
對(duì)于這個(gè)原因厌处,首先要做的是嘗試扁平化你的對(duì)象录豺。需要關(guān)注的是朦肘,React官方推薦根據(jù)哪些值傾向于一起變化,將狀態(tài)分割成多個(gè)狀態(tài)變量双饥。
如果這個(gè)不可能的話媒抠,推薦使用第三方庫(kù)來(lái)幫助你使用不可變對(duì)象,例如immutable.js或者 immer
useState使用規(guī)則
useState
和所有的Hooks一樣咏花,都遵循同樣的規(guī)則:
- 在頂層調(diào)用Hooks
- 在React函數(shù)中調(diào)用Hooks
第二個(gè)規(guī)則很容易理解趴生,不要在類組件中使用useState
:
或者在常規(guī)JS方法中(不能在一個(gè)方法組件中被調(diào)用的):
如果項(xiàng)目中有ESLint的話, 則可以看到對(duì)應(yīng)錯(cuò)誤提示昏翰;如果沒(méi)有苍匆,則可以從該文檔中看到出現(xiàn)的錯(cuò)誤提示。
第一個(gè)規(guī)則指的是:即使在函數(shù)組件內(nèi)部棚菊,不能在循環(huán)浸踩、條件或者內(nèi)嵌方法中調(diào)用useState
,因?yàn)镽eact依賴于useState
函數(shù)被調(diào)用的順序來(lái)獲取特定變量的正確值统求。
在這方面检碗,常見的錯(cuò)誤是据块,在if語(yǔ)句使用useState
(它們不是每次都被執(zhí)行的):
if (condition) { // 有時(shí)它會(huì)執(zhí)行,導(dǎo)致useState的調(diào)用順序發(fā)生變化
const [message, setMessage] = useState( '' );
setMessage( aMessage );
}
const [list, setList] = useState( [] );
setList( [1, 2, 3] );
函數(shù)組件中會(huì)多次調(diào)用useState
或者其他的Hooks折剃。每個(gè)Hook是存儲(chǔ)在鏈表中的另假,而且有一個(gè)變量來(lái)追蹤當(dāng)前執(zhí)行的Hook。
當(dāng)useState
被執(zhí)行時(shí)怕犁,當(dāng)前Hook的狀態(tài)被讀缺呃骸(第一次渲染期間是被初始化),之后奏甫,變量被改變?yōu)橹赶蛳乱粋€(gè)Hook戈轿。
這也就是為什么總是始終保持Hook相同的調(diào)用順序是很重要的。否則的話阵子,狀態(tài)值就會(huì)屬于另一個(gè)狀態(tài)變量凶杖。
總體來(lái)說(shuō),這兒有一個(gè)例子來(lái)一步步說(shuō)明它是如何工作的:
- React初始化Hook鏈表款筑,并且用一個(gè)變量追蹤當(dāng)前Hook
- React首次調(diào)用你的組件
- React發(fā)現(xiàn)了
useState
的調(diào)用智蝠,創(chuàng)建了一個(gè)新的Hook對(duì)象(帶有初始狀態(tài)),將當(dāng)前Hook變量指向該Hook對(duì)象奈梳,將該對(duì)象添加到Hooks鏈表中杈湾,然后返回一個(gè)帶有初始值和更新狀態(tài)方法的數(shù)組 - React發(fā)現(xiàn)另一個(gè)
useState
的調(diào)用,重復(fù)上面的步驟攘须,存儲(chǔ)新的hook對(duì)象漆撞,改變當(dāng)前Hook變量。 - 組件狀態(tài)發(fā)生變化
- React給要處理的隊(duì)列發(fā)送新的狀態(tài)更新操作(執(zhí)行
useState
返回的方法) - React決定組件是否需要重新渲染
- React重置當(dāng)前Hook變量于宙,并且調(diào)用組件
- React發(fā)現(xiàn)了一個(gè)
useState
的調(diào)用浮驳,但此時(shí),在Hooks鏈表第一位置已經(jīng)有一個(gè)Hook捞魁,它僅僅改變了當(dāng)前Hook變量至会,返回一個(gè)帶狀態(tài)值和更新狀態(tài)方法的數(shù)組 - React發(fā)現(xiàn)另一個(gè)
useState
的調(diào)用,因?yàn)榈诙€(gè)位置有一個(gè)Hook了谱俭,它再次僅僅改變了當(dāng)前Hook變量奉件,返回一個(gè)帶狀態(tài)值和更新狀態(tài)方法的數(shù)組
這是一個(gè)簡(jiǎn)單的useState工作流程,具體可以看ReactFiberHooks源碼
useReducer
Hook的使用
對(duì)于很多高級(jí)使用情況昆著,我們可以使用useReducer
Hook來(lái)代替useState
县貌。當(dāng)處理復(fù)雜的狀態(tài)邏輯時(shí),它是很有用的凑懂。比如包含多個(gè)子值或者狀態(tài)依賴之前的值煤痕。
Look,看下如何使用useReducer
Hook,示例:
const [state, dispatch] = useReducer(reducer, initialArgument, init);
//useReducer返回一個(gè)帶有當(dāng)前狀態(tài)值和dispatch方法的數(shù)組摆碉。如果你使用過(guò)Redux祟敛,這個(gè)就得心應(yīng)手了。
在useState
兆解,你調(diào)用更新狀態(tài)的方法,然而useReducer
中跑揉,你調(diào)用dispatch
方法锅睛,然后傳遞給它一個(gè)action,eg: 至少是帶有一個(gè)type
屬性的對(duì)象历谍。
dispatch({type:"increase"})
一般來(lái)講现拒,一個(gè)action對(duì)象也會(huì)有其他的屬性,eg: {action: "increase", payload: "10"}
望侈。
然而傳遞一個(gè)action對(duì)象并不是絕對(duì)的印蔬,具體參考Redux
總結(jié)
uesState
是一個(gè)Hook函數(shù),它讓你可以在組件中擁有狀態(tài)變量脱衙,你可以給這個(gè)方法傳遞初始值侥猬,并且返回一個(gè)當(dāng)前狀態(tài)值的變量(不一定是初始值)和另一個(gè)更新該值的方法。
這兒有一些需要記住的關(guān)鍵的點(diǎn):
- 更新方法不會(huì)立即更新值
- 如果你需要用到之前的值更新狀態(tài)捐韩,你必須將之前的值傳遞給該方法退唠,則它會(huì)返回一個(gè)更新后的值,eg:
setMessage(previousVal => previousVal + currentVal)
- 如果你使用同樣的值作為當(dāng)前狀態(tài)荤胁,則React不會(huì)觸發(fā)重新渲染瞧预。(React是用
Object.is
來(lái)做比較的) -
useState
不像類組件中的this.setState
合并對(duì)象,它是直接替換對(duì)象仅政。 -
useState
和所有的hooks遵循相同的規(guī)則垢油,特別是,注意這些函數(shù)的調(diào)用順序圆丹。(可以借助 ESLint plugin滩愁,它會(huì)幫助我們強(qiáng)制實(shí)施這些規(guī)則)