React useState使用完整教程(译)
useState
是一个Hook函数,让你在函数组件中拥有state变量。它接收一个初始化的state,返回是一个数组,数组里有两个元素,第一个元素是当前状态值和另一个更新该值的方法。
本教程主要是针对于React中的useState
做一个详细的描述,它等同于函数组件中的this.state
/this.setState
,我们将会围绕下面的问题逐一解析:
- React中的类组件和函数组件
- React.useState hook做了什么
- 在React中声明状态
- React Hooks: Update State
- 在useState hook中使用对象作为状态变量
- React Hooks中如何更新嵌套对象状态
- 多个状态变量还是一个状态对象
- 使用useState的规则
- useReducer Hook的使用
如果您是刚开始学习使用useState,请查看官方文档或者该视频教程。
React中的类组件和函数组件
React有两种类型的组件:类组件和函数组件。
类组件是继承React.Component
的ES6类,它有自己的状态和生命周期函数:
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
message: ""
};
}
componentDidMount() {
/*...*/
}
render() {
return <div>{this.state.message}</div>
}
}
函数组件是一个函数,它能接收任何组件的属性作为参数,并且可以返回有效的JSX。
function Message(props) {
return <div>{props.message} </div>
}
//或者使用箭头函数
const Message = (props) => <div>{props.message}</div>
正如所看到的,函数组件没有任何状态和生命周期方法。不过,在React16.8,我们可以使用Hooks。
React Hooks是方法,它可以给函数组件添加状态变量,并且可以模拟类组件的生命周期方法。他们倾向以use作为Hook名的开始。
React.useState hook做了什么
正如之前了解的,useState
可以给函数组件添加状态,函数组件中的useState
可以生成一系列与其组件相关联的状态。
类组件中的状态总是一个对象,不过Hooks中的状态可以是任意类型。每个state可以有单一的值,也可以是一个对象、数组、布尔值或者能想到的任意类型。
So,你会什么时候用useState
Hook呢?它对组件自身的状态很有用,然而大项目可能会需要另外的状态管理方案。
React声明状态
useState
是React
的命名输出出,因此你可以这么写:
React.useState
或者可以直接这么写:
import React, { useState } from "react";
然而不像在类组件里声明状态对象那样,useState
允许声明多个状态变量:
import React from "react";
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "",
list: "",
}
}
/*...*/
}
useState
Hook一次只能声明一个状态变量,不过这个状态变量可以是任意类型的:
import React, { useState } from "react";
const Message = () => {
const messageState = useState("");
const listState = useState([]);
}
useState
接收状态的初始值作为一个参数。
正如之前例子展示的,可以直接给函数传递,也可以使用函数来延迟初始化该变量(当初始化状态基于一次昂贵的计算,这种方式是很有用的):
const Message = () => {
const messageState = useState(() => expensiveComputation());
/*...*/
}
初始化的值仅仅会在第一次渲染时被赋值(如果他是一个函数,也是会在初次渲染时执行)。
在后续的更新中(由于组件本身的状态更改或者是说父组件导致的变化),useState
Hook参数(初始值)将会被忽略,当前的值将会被使用。
理解它是非常重要的,举个例子,如果你想更新基于组件接收的新属性的状态:
const Message = (props) => {
const messageState = useState(props.message);
}
只单独使用useState
不会工作的,因为它的参数仅仅在第一次生效,并不是每次属性更改时生效(可以结合useEffect
使用,具体查看该回答)
不过,useState
不是像之前所说的仅仅返回一个变量。
它返回的是一个数组,第一个元素是状态变量,第二个元素是更新该变量值的方法。
const Message= () => {
const messageState = useState("");
const message = messageState[0]; // 是一个空字符串
const setMessage = messageState[1]; // 是一个方法
}
一般我们会选择数组解构的方式来简化上述代码:
const Message = () => {
const [message, setMessage] = useState("");
}
在函数组件中可以像其他变量一样使用状态变量:
const Message = () => {
const [message, setMessage] = useState("");
return <p>{message}</p>;
}
但是为什么useState
会返回一个数组呢?
因为与对象相比、数组是非常灵活且容易使用。
如果这个方法返回的是一个包括一系列属性集的对象,那么就不能很容易自定义变量名,比如:
// 没有使用对象解构
const messageState = useState( '' );
const message = messageState.state;
const setMessage = messageState;
//使用对象解构
const { state: message, setState: setMessage } = useState( '' );
const { state: list, setState: setList } = useState( [] );
React Hooks: 更新状态
useState
返回的第二个元素是一个方法,它用新值来更新状态变量。
举个🌰,使用输入框在每次改变时更新状态变量示例:
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>
);
};
然而,这个更新函数不会立即更新值。相反它会排队等待更新操作。在重新渲染组件后,useState
的参数将被忽略,这个更新方法将会返回最新的值。
如果你需要用之前的值来更新状态,你一定得传递一个接收之前值的方法来返回新值示例:
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中使用对象作为状态变量
当使用对象时,需要记住的是:
- 不可变的重要性
useState
返回的更新方法不是像类组件中的setState
合并对象
关于第一点,如果你是用相同的值作为当前值来更新state(React使用的Object.is
来做比较),React不会触发更新的。
当使用对象时,很容易出现下面的错误示例,输入框不能输入文本:
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>
);
};
上面的例子没有创建一个新对象,而是改变已经存在的状态对象。对于React来说,它们是同一个对象。为了正常运行,我们创建一个新对象示例:
onChange={(e) => {
const newMessageObj = { message: e.target.value };
setMessageObj(newMessageObj);
}}
这个让我们看到了你需要记住的第二件事情。
当你创建一个状态变量时,类组件中的this.setState
自动合并更新对象,而函数组件中useState
的更新方法则是直接替换对象 。
继续上面的例子,如果我们给message
对象添加一个id
属性,将会发生什么呢示例:
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>
);
};
当只更新message
属性时,React将替换原先的状态值{ message: '', id: 1 }
,当触发onChange
属性时,状态值将仅仅包含message
属性:
{message: "····"} // id属性丢失了
当然,通过替换的对象和扩展运算之前的对象结合作为参数也可以在函数组件中复制setState()
的行为示例:
onChange={(e) => {
const val = e.target.value;
setMessageObj((prevState) => {
return { ...prevState, message: val };
});
}}
...prevState
会得到对象所有的属性,message: val
会重新赋值给message
属性。
当然使用Object.assign
也会得到相同的结果(需要创建新对象)示例:
//使用Object.assign
setMessageObj((prevState) => {
return Object.assign({}, prevState, { message: val });
});
不过扩展运算可以简化这个操作,而且也可以应用到数组上。
一般来讲,当在数组上使用时,扩展运算移除了括号,你可以用旧数组中的值创建另一个数组:
[
...['a', 'b', 'c'],
'd'
]
// Is equivalent to
[ 'a', 'b', 'c','d']
再来个🌰,如何用数组来使用useState
示例:
需要注意的是,处理多维数组时需要谨慎的使用扩展运算,因为可能最终的结果不是你所期待的。
所以这个时候,我们就需要考虑使用对象作为状态。
React Hooks中如何更新嵌套对象状态
JS中,多维数组是数组里嵌套数组:
[
['value1','value2'],
['value3','value4']
]
你可以在用它们来把你所有的状态变量集中在一个地方,然而,为了这个目的,最好使用内嵌对象:
{
'row1' : {
'key1' : 'value1',
'key2' : 'value2'
},
'row2' : {
'key3' : 'value3',
'key4' : 'value4'
}
}
当使用内嵌对象和多维数组时,有一个问题是Object.assign
和扩展运算是创建了一个浅拷贝并非是深拷贝。
当拷贝数组时,扩展运算仅仅做了一层的拷贝,因此,对于多维数组来说,使用它是不合适的,就像下面例子中所示(使用
Object.assign()
和扩展元算结果都是true):
let a = [[1], [2], [3]];
let b = [...a];
b.shift().shift(); // 1
//此时数组a的结果是[[], [2], [3]]
StackOverflow关于上面的例子提供了一个比较好的解释,不过目前重要的是,当使用内嵌对象时,我们不能用扩展运算来更新状态对象。
再举个🌰
const [messageObj, setMessageObj] = useState({
author: "",
message: {
id: 1,
text: "",
}
});
来,先看看更新text
字段的错误方式示例:
//错误, 文本更改后,messageObj的值是{author: "", message: {id: 1, text: ""}, text: "*"}
setMessageObj((prevState) => ({
...prevState,
text: val,
}));
//错误,文本更改后messageObj值是{id: "*", text: "*"}
setMessageObj((prevState) => ({
...prevState.message,
text: val
}));
//错误,文本更改后,messageObj的值是{author: "", message: {text: "*"}},缺少id属性
setMessageObj((prevState) => ({
...prevState,
message: {
text: val,
}
}));
为了能正确的更新text
属性,我们需要拷贝一个原始对象,这个新对象包括整个原始对象的所有属性:
setMessageObj((prevState) => ({
...prevState, //赋值第一层的key值
message: { //创建包含更新key值的对象
...prevState.message, //复制包含key值的对象
text: val, //给需要更新的字段重新赋值
}
}));
以同样的方式,我们也可以更新author
字段:
setMessageObj((prevState) => ({
author: "Joe",
message: { ...prevState.message },
}));
如果message
对象变化了,则用以下的方式:
setMessageObj((prevState) => ({
author: "Joe",
message: {
...prevState.message,
text: val,
},
}));
声明多个状态变量还是一个状态对象
当你的应用中使用多个字段或值作为状态变量时,你可以选择组织多个变量:
const [id, setId] = useState(-1);
const [message, setMessage] = useState('');
const [author, setAuthor] = useState('');
或者使用一个对象状态变量:
const [messageObj, setMessage] = useState({
id: 1,
message: '',
author: ''
});
不过,需要谨慎的使用复杂数据结构(内嵌对象)的状态对象,考虑下这个🌰 :
const [messageObj, setMessage] = useState({
input: {
author: {
id: -1,
author: {
fName:'',
lName: ''
}
},
message: {
id: -1,
text: '',
date: new Date(),
}
}
});
如果你需要更新嵌套在对象深处的指定字段时,你必须复制所有其他对象和包含该指定字段的的对象的键值对一起复制:
setMessage(prevState => ({
input: {
...prevState.input,
message: {
...prevState.input.message,
text: '***',
}
}
}));
在某些情况下,拷贝深度内嵌对象是比较昂贵的,因为React可能会依赖那些没有改变过的字段值重新渲染你应用的部分内容。
对于这个原因,首先要做的是尝试扁平化你的对象。需要关注的是,React官方推荐根据哪些值倾向于一起变化,将状态分割成多个状态变量。
如果这个不可能的话,推荐使用第三方库来帮助你使用不可变对象,例如immutable.js或者 immer
useState使用规则
useState
和所有的Hooks一样,都遵循同样的规则:
- 在顶层调用Hooks
- 在React函数中调用Hooks
第二个规则很容易理解,不要在类组件中使用useState
:
或者在常规JS方法中(不能在一个方法组件中被调用的):
如果项目中有ESLint的话, 则可以看到对应错误提示;如果没有,则可以从该文档中看到出现的错误提示。
第一个规则指的是:即使在函数组件内部,不能在循环、条件或者内嵌方法中调用useState
,因为React依赖于useState
函数被调用的顺序来获取特定变量的正确值。
在这方面,常见的错误是,在if语句使用useState
(它们不是每次都被执行的):
if (condition) { // 有时它会执行,导致useState的调用顺序发生变化
const [message, setMessage] = useState( '' );
setMessage( aMessage );
}
const [list, setList] = useState( [] );
setList( [1, 2, 3] );
函数组件中会多次调用useState
或者其他的Hooks。每个Hook是存储在链表中的,而且有一个变量来追踪当前执行的Hook。
当useState
被执行时,当前Hook的状态被读取(第一次渲染期间是被初始化),之后,变量被改变为指向下一个Hook。
这也就是为什么总是始终保持Hook相同的调用顺序是很重要的。否则的话,状态值就会属于另一个状态变量。
总体来说,这儿有一个例子来一步步说明它是如何工作的:
- React初始化Hook链表,并且用一个变量追踪当前Hook
- React首次调用你的组件
- React发现了
useState
的调用,创建了一个新的Hook对象(带有初始状态),将当前Hook变量指向该Hook对象,将该对象添加到Hooks链表中,然后返回一个带有初始值和更新状态方法的数组 - React发现另一个
useState
的调用,重复上面的步骤,存储新的hook对象,改变当前Hook变量。 - 组件状态发生变化
- React给要处理的队列发送新的状态更新操作(执行
useState
返回的方法) - React决定组件是否需要重新渲染
- React重置当前Hook变量,并且调用组件
- React发现了一个
useState
的调用,但此时,在Hooks链表第一位置已经有一个Hook,它仅仅改变了当前Hook变量,返回一个带状态值和更新状态方法的数组 - React发现另一个
useState
的调用,因为第二个位置有一个Hook了,它再次仅仅改变了当前Hook变量,返回一个带状态值和更新状态方法的数组
这是一个简单的useState工作流程,具体可以看ReactFiberHooks源码
useReducer
Hook的使用
对于很多高级使用情况,我们可以使用useReducer
Hook来代替useState
。当处理复杂的状态逻辑时,它是很有用的。比如包含多个子值或者状态依赖之前的值。
Look,看下如何使用useReducer
Hook,示例:
const [state, dispatch] = useReducer(reducer, initialArgument, init);
//useReducer返回一个带有当前状态值和dispatch方法的数组。如果你使用过Redux,这个就得心应手了。
在useState
,你调用更新状态的方法,然而useReducer
中,你调用dispatch
方法,然后传递给它一个action,eg: 至少是带有一个type
属性的对象。
dispatch({type:"increase"})
一般来讲,一个action对象也会有其他的属性,eg: {action: "increase", payload: "10"}
。
然而传递一个action对象并不是绝对的,具体参考Redux
总结
uesState
是一个Hook函数,它让你可以在组件中拥有状态变量,你可以给这个方法传递初始值,并且返回一个当前状态值的变量(不一定是初始值)和另一个更新该值的方法。
这儿有一些需要记住的关键的点:
- 更新方法不会立即更新值
- 如果你需要用到之前的值更新状态,你必须将之前的值传递给该方法,则它会返回一个更新后的值,eg:
setMessage(previousVal => previousVal + currentVal)
- 如果你使用同样的值作为当前状态,则React不会触发重新渲染。(React是用
Object.is
来做比较的) -
useState
不像类组件中的this.setState
合并对象,它是直接替换对象。 -
useState
和所有的hooks遵循相同的规则,特别是,注意这些函数的调用顺序。(可以借助 ESLint plugin,它会帮助我们强制实施这些规则)