
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的使用
如果您是剛開(kāi)始學(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名的開(kāi)始。
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í)候用useStateHook呢?它對(duì)組件自身的狀態(tài)很有用,然而大項(xiàng)目可能會(huì)需要另外的狀態(tài)管理方案。
React聲明狀態(tài)
useState是React的命名輸出出,因此你可以這么寫(xiě):
React.useState
或者可以直接這么寫(xiě):
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: "",
}
}
/*...*/
}
useStateHook一次只能聲明一個(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)致的變化),useStateHook參數(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īng)顟B(tài)
useState返回的第二個(gè)元素是一個(gè)方法,它用新值來(lái)更新?tīng)顟B(tài)變量。
舉個(gè)??,使用輸入框在每次改變時(shí)更新?tīng)顟B(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īng)顟B(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īng)顟B(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)獲取特定變量的正確值。
在這方面,常見(jiàn)的錯(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īng)顟B(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īng)顟B(tài)方法的數(shù)組 - React發(fā)現(xiàn)另一個(gè)
useState的調(diào)用,因?yàn)榈诙€(gè)位置有一個(gè)Hook了,它再次僅僅改變了當(dāng)前Hook變量,返回一個(gè)帶狀態(tài)值和更新?tīng)顟B(tài)方法的數(shù)組
這是一個(gè)簡(jiǎn)單的useState工作流程,具體可以看ReactFiberHooks源碼
useReducerHook的使用
對(duì)于很多高級(jí)使用情況,我們可以使用useReducerHook來(lái)代替useState。當(dāng)處理復(fù)雜的狀態(tài)邏輯時(shí),它是很有用的。比如包含多個(gè)子值或者狀態(tài)依賴之前的值。
Look,看下如何使用useReducerHook,示例:
const [state, dispatch] = useReducer(reducer, initialArgument, init);
//useReducer返回一個(gè)帶有當(dāng)前狀態(tài)值和dispatch方法的數(shù)組。如果你使用過(guò)Redux,這個(gè)就得心應(yīng)手了。
在useState,你調(diào)用更新?tīng)顟B(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īng)顟B(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ī)則)