State Hook
首先引入 React 中 useState 的 Hook
import React, { useState } from 'react';useState
const [state, setState] = useState(initialState);返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState);在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
注意
React 会确保
setState函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从useEffect或useCallback的依赖列表中省略setState。
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:
function Counter({initialCount}) {
const \[count, setCount\] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);}“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值。
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
注意
与 class 组件中的
setState方法不同,useState不会自动合并更新对象。你可以用函数式的setState结合展开运算符来达到合并更新对象的效果。setState(prevState => { // 也可以使用 Object.assign return {...prevState, ...updatedValues};});
useReducer是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
惰性初始 state
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const \[state, setState\] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;});跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
为什么 useState 要使用数组而不是对象
这里用到了解构赋值,所以先来看一下ES6 的解构赋值:
数组的解构赋值
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3对象的解构赋值
const user = {
id: 666,
name: "hello"
};
const { id, name } = user;
console.log(id); // 666
console.log(name); // "hello"看完这两个例子,答案应该就出来了:
- 如果 useState 返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净
- 如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值
useState高级用法
恢复默认值
组件需求:实现一个计数器,有3个按钮,点击后分别实现:恢复默认值、点击+1、点击-1
实现代码:
import React, { useState } from 'react';
function Component() {
const initCount = 0;
const [count, setCount] = useState(initCount);
return <div>
{count}
<button onClick={() => {setCount(initCount)}}>init</button>
<button onClick={() => {setCount(count+1)}}>+1</button>
<button onClick={() => {setCount(count-1)}}>-1</button>
</div>
}
export default Component;代码分析:
通过额外定义一个变量initCount=0,作为count的默认值;
任何时候想恢复默认值,直接将initCount赋值给count;
解决数据异步
for(let i=0; i<3; i++){
setCount(count+1);
}通过for循环,执行了3次setCount(count+1),那么你觉得count会 +3 吗?
答案是:肯定不会
无论for循环执行几次,最终实际结果都将是仅仅执行一次 +1。
为什么?
类组件中setState赋值过程是异步的,同样在Hook中 setXxx 赋值也是异步的,比如上述代码中的setCount。
虽然执行了3次setCount(count+1),可是每一次修改后的count并不是立即生效的。当第2次和第3次执行时获取到count的值和第1次获取到的count值是一样的,所以最终其实相当于仅执行了1次。
解决办法:
for(let i=0; i<3; i++){
setCount(prevData => prevData+1);
}代码分析:
- prevData为我们定义的一个形参,指当前count应该的值;
{return prevData+1}中,将prevData+1,并将运算结果return出去。- 最终将prevData赋值给count;
补充说明: 你可以将prevData修改成任意你喜欢的变量名称,比如prev,只需要确保和后面return里的一致即可。
数据类型为Objcet的修改方法
useState更新状态不会把新的state和旧的state合并,而是会直接覆盖整个状态,如果state中保存的是一个对象,在更新的时候需要注意使用解构赋值来确保修改state的完整性,另外通过useState 来修改状态的时候如果状态值是对象或数组,React会进行浅比较,如果浅比较没有发生变化则不会引发组件的刷新
const [person, setPerson] = useState({name:'stream',age:20});若想将age的值修改为18,该怎么写?
正确的做法:
我们需要先将person拷贝一份,修改之后再进行赋值。
let newData = {...person};
newData.age = 18;
setPerson(newData);以上代码还有一种简写形式:
setPerson({...person,age:18}); //解构赋值代码分析:
- 先通过...person,将原有person做一次解构,得到一份深拷贝;
- 修改age的值;
- 将修改过后的新数据,通过setPerson赋值给person;
数据类型为Array的修改方法
和数据类型为Object相似,都是需要通过先拷贝一次,修改后再整体赋值。
性能优化
通过 setSth 设置新值,但是如果新值和当前值完全一样,那么会引发React重新渲染吗?
通过React官方文档可以知道,当使用 setSth 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染。