以下内容皆基于react18
setState有两种形式,一种是就传个新的State进去,像这样:
setState(n + 1)
另一种是传一个函数进去,像这样:
setState(n => n + 1);
1. 直接传参
区别在哪?来看个例子:
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
这个例子中每次点击button,number都只会+1,为什么?
因为state实际上存的是当前渲染的快照,也就是说这次渲染中的setState,在下次渲染中才能体现出来,因此实际上这段代码的三行setNumber()
是被这么处理的:
-
setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。
React 准备在下一次渲染时将 number 更改为 1。 -
setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
React 准备在下一次渲染时将 number 更改为 1。 -
setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
React 准备在下一次渲染时将 number 更改为 1。
2. 传入函数
那么怎么才能点一次按钮就+3呢,很简单,采用传入函数的形式,像这样:
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
如果是这样,那么react会:
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
那么队列中就会:
这样就实现了点击一次+3。
3. 混合
那么假如将两者混起来,也是一样的道理,比如这个例子:
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>增加数字</button>
</>
)
}
队列中就是这样的:
所以最终结果是6。
4. 练习
最后看道练习题:
你正在开发一个艺术市场应用,该应用允许一个用户为一个艺术品同时提交多个订单。每次用户按下“购买”按钮,“等待”计数器应该增加一。三秒后,“等待”计数器应该减少,“完成”计数器应该增加。
但是,“等待”计数器的行为并不符合预期。当你按下“购买”按钮时,它会减少到 -1(这本应该是不可能的)。如果你快速点击两次,两个计数器似乎都会出现无法预测的行为。
为什么会发生这种情况?修复两个计数器。
sanbox
很简单,会出现-1是因为如果直接传参,不过点击多少下按钮,当前渲染的state都只会记住初始值0,并在下一个渲染中执行pending - 1,就会产生-1。
将setState改成传入函数形式即可:
async function handleClick() {
setPending(pending => pending + 1);
await delay(3000);
setPending(pending => pending - 1);
setCompleted(completed + 1);
}
5. useEffect中的setState
通过函数来setState有时候还有另一个好处,先来看这段代码:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]);
connection监听用户发送的信息,每发一条信息就会触发setMessages,看起来没什么问题。
然而,这让messages成了useEffect的依赖,会导致用户每发一条信息,连接就要重启一次。 这无疑是不可接受的用户体验。
解决方案是把setState换成函数传参的方式:
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
这样一来,useEffect的依赖就不用包含messages了,解决了上面的问题。