setState 的真实世界用法与更新回调而不是在 React JS 中传递对象
Real world usage of setState with an updater callback instead of passing an object in React JS
React 文档对 setState 的描述如下:
If you need to set the state based on the previous state, read about the updater argument below
、
除了下面这句话,我没看懂:
If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
他们说:
The first argument is an updater function with the signature (state, props) => stateChange ... state is a reference to the component state at the time the change is being applied.
并举个例子:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
说:
Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.
保证是最新的 是什么意思,在决定是否应该将 setState
与更新函数一起使用时我们应该注意什么(state, props) => stateChange
还是直接用对象作为第一个参数?
让我们假设一个真实的场景。假设我们有一个花哨的聊天应用程序,其中:
- 聊天状态由
this.state = { messages: [] }
; 表示
- 先前的消息已加载,发出 AJAX 请求,并被添加到当前处于该状态的
messages
之前;
- 如果其他用户(不是当前用户)向当前用户发送消息,新消息将从实时 WebSocket 连接到达当前用户,并附加到当前状态的
messages
;
- 如果发送消息的是当前用户,则当 AJAX 请求被触发时,消息将附加到状态的
messages
,如第 3 点发送完成;
假设这是我们的 FancyChat
组件:
import React from 'react'
export default class FancyChat extends React.Component {
constructor(props) {
super(props)
this.state = {
messages: []
}
this.API_URL = 'http://...'
this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
this.handleNewMessageFromOtherUser = this.handleNewMessageFromOtherUser.bind(this)
this.handleNewMessageFromCurrentUser = this.handleNewMessageFromCurrentUser.bind(this)
}
componentDidMount() {
// Assume this is a valid WebSocket connection which lets you add hooks:
this.webSocket = new FancyChatWebSocketConnection()
this.webSocket.addHook('newMessageFromOtherUsers', this.handleNewMessageFromOtherUser)
}
handleLoadPreviousChatMessages() {
// Assume `AJAX` lets you do AJAX requests to a server.
AJAX(this.API_URL, {
action: 'loadPreviousChatMessages',
// Load a previous chunk of messages below the oldest message
// which the client currently has or (`null`, initially) load the last chunk of messages.
below_id: (this.state.messages && this.state.messages[0].id) || null
}).then(json => {
// Need to prepend messages to messages here.
const messages = json.messages
// Should we directly use an updater object:
this.setState({
messages: messages.concat(this.state.messages)
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: messages.concat(state.messages)
.sort(this.sortByTimestampComparator)
}
})
// What if while the user is loading the previous messages, it also receives a new message
// from the WebSocket channel?
})
}
handleNewMessageFromOtherUser(data) {
// `message` comes from other user thanks to the WebSocket connection.
const { message } = data
// Need to append message to messages here.
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
// Assume `sentTimestamp` is a centralized Unix timestamp computed on the server.
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
}
handleNewMessageFromCurrentUser(messageToSend) {
AJAX(this.API_URL, {
action: 'newMessageFromCurrentUser',
message: messageToSend
}).then(json => {
// Need to append message to messages here (message has the server timestamp).
const message = json.message
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
// What if while the current user is sending a message it also receives a new one from other users?
})
}
sortByTimestampComparator(messageA, messageB) {
return messageA.sentTimestamp - messageB.sentTimestamp
}
render() {
const {
messages
} = this.state
// Here, `messages` are somehow rendered together with an input field for the current user,
// as well as the above event handlers are passed further down to the respective components.
return (
<div>
{/* ... */}
</div>
)
}
}
有这么多异步操作,我如何才能真正确定 this.state.messages
将始终与服务器上的数据一致,我将如何针对每种情况使用 setState
?我应该考虑什么?我应该始终使用 setState
的 updater
函数(为什么?)还是直接将对象作为 updater
参数传递是安全的(为什么?)?
感谢关注!
setState
只关心组件状态一致性,不关心server/client一致性。因此 setState
不保证组件状态与其他任何状态一致。
之所以提供更新函数,是因为状态更新有时会延迟,并且不会在调用 setState
时立即发生。因此,如果没有 updater 函数,您基本上会遇到竞争条件。例如:
- 您的组件以
state = {counter: 0}
开头
- 您有一个按以下方式单击时更新计数器的按钮:
this.setState({counter: this.state.counter +1})
- 用户点击按钮的速度非常快,因此状态没有时间在点击之间更新。
- 这意味着计数器只会增加 1,而不是预期的 2 - 假设计数器最初为 0,两次单击按钮,调用最终为
this.setState({counter: 0+1})
,设置状态两次都为 1。
更新程序功能修复了这个问题,因为更新是按顺序应用的:
- 您的组件以
state = {counter: 0}
开头
- 您有一个按以下方式单击时更新计数器的按钮:
this.setState((currentState, props) => ({counter: currentState.counter + 1}))
- 用户点击按钮的速度非常快,因此状态没有时间在点击之间更新。
- 与其他方式不同,
currentState.counter + 1
不会立即得到评估
- 使用初始状态
{counter: 0}
调用第一个更新程序函数,并将状态设置为 {counter: 0+1}
- 使用状态
{counter: 1}
调用第二个更新程序函数,并将状态设置为 {counter: 1+1}
一般来说,updater 函数是较少 error-prone 改变状态的方式,很少有理由不使用它(尽管如果你设置的是静态值,你并不严格需要它)。
然而,您关心的是对状态的更新不会导致不正确的数据(重复等)。在那种情况下,我会注意更新的设计,以便它们是幂等的,并且无论数据的当前状态如何都可以工作。例如,不是使用数组来保存消息集合,而是使用映射,并通过该消息唯一的键或哈希存储每条消息,无论它来自哪里(毫秒时间戳可能足够唯一) .那么,当你从两个位置获取相同的数据时,就不会造成重复。
无论如何我都不是 React 方面的专家,而且只做了两个月,但这是我从我在 React 中的第一个项目中学到的东西,它就像随机引用一样简单。
如果您需要在使用 setState 后立即使用更新状态,请始终使用 updater 函数。让我举一个例子。
//
handleClick = () => {
//get a random color
const newColor = this.selectRandomColor();
//Set the state to this new color
this.setState({color:newColor});
//Change the background or some elements color to this new Color
this.changeBackgroundColor();
}
我这样做了,发生的事情是设置到正文的颜色始终是以前的颜色,而不是状态中的当前颜色,因为如您所知,setState
是批处理的。它发生在 React 认为最好执行它的时候。它不会立即执行。所以要解决这个问题,我所要做的就是将 this.changeColor
作为第二个参数传递给 setState。因为这确保了我应用的颜色与当前状态保持同步。
所以回答你的问题,在你的情况下,因为你的工作是在新消息到达时立即向用户显示消息,即使用更新状态,始终使用更新程序功能而不是目的。
React 文档对 setState 的描述如下:
If you need to set the state based on the previous state, read about the updater argument below
、
除了下面这句话,我没看懂:
If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
他们说:
The first argument is an updater function with the signature (state, props) => stateChange ... state is a reference to the component state at the time the change is being applied.
并举个例子:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
说:
Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.
保证是最新的 是什么意思,在决定是否应该将 setState
与更新函数一起使用时我们应该注意什么(state, props) => stateChange
还是直接用对象作为第一个参数?
让我们假设一个真实的场景。假设我们有一个花哨的聊天应用程序,其中:
- 聊天状态由
this.state = { messages: [] }
; 表示
- 先前的消息已加载,发出 AJAX 请求,并被添加到当前处于该状态的
messages
之前; - 如果其他用户(不是当前用户)向当前用户发送消息,新消息将从实时 WebSocket 连接到达当前用户,并附加到当前状态的
messages
; - 如果发送消息的是当前用户,则当 AJAX 请求被触发时,消息将附加到状态的
messages
,如第 3 点发送完成;
假设这是我们的 FancyChat
组件:
import React from 'react'
export default class FancyChat extends React.Component {
constructor(props) {
super(props)
this.state = {
messages: []
}
this.API_URL = 'http://...'
this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
this.handleNewMessageFromOtherUser = this.handleNewMessageFromOtherUser.bind(this)
this.handleNewMessageFromCurrentUser = this.handleNewMessageFromCurrentUser.bind(this)
}
componentDidMount() {
// Assume this is a valid WebSocket connection which lets you add hooks:
this.webSocket = new FancyChatWebSocketConnection()
this.webSocket.addHook('newMessageFromOtherUsers', this.handleNewMessageFromOtherUser)
}
handleLoadPreviousChatMessages() {
// Assume `AJAX` lets you do AJAX requests to a server.
AJAX(this.API_URL, {
action: 'loadPreviousChatMessages',
// Load a previous chunk of messages below the oldest message
// which the client currently has or (`null`, initially) load the last chunk of messages.
below_id: (this.state.messages && this.state.messages[0].id) || null
}).then(json => {
// Need to prepend messages to messages here.
const messages = json.messages
// Should we directly use an updater object:
this.setState({
messages: messages.concat(this.state.messages)
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: messages.concat(state.messages)
.sort(this.sortByTimestampComparator)
}
})
// What if while the user is loading the previous messages, it also receives a new message
// from the WebSocket channel?
})
}
handleNewMessageFromOtherUser(data) {
// `message` comes from other user thanks to the WebSocket connection.
const { message } = data
// Need to append message to messages here.
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
// Assume `sentTimestamp` is a centralized Unix timestamp computed on the server.
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
}
handleNewMessageFromCurrentUser(messageToSend) {
AJAX(this.API_URL, {
action: 'newMessageFromCurrentUser',
message: messageToSend
}).then(json => {
// Need to append message to messages here (message has the server timestamp).
const message = json.message
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
// What if while the current user is sending a message it also receives a new one from other users?
})
}
sortByTimestampComparator(messageA, messageB) {
return messageA.sentTimestamp - messageB.sentTimestamp
}
render() {
const {
messages
} = this.state
// Here, `messages` are somehow rendered together with an input field for the current user,
// as well as the above event handlers are passed further down to the respective components.
return (
<div>
{/* ... */}
</div>
)
}
}
有这么多异步操作,我如何才能真正确定 this.state.messages
将始终与服务器上的数据一致,我将如何针对每种情况使用 setState
?我应该考虑什么?我应该始终使用 setState
的 updater
函数(为什么?)还是直接将对象作为 updater
参数传递是安全的(为什么?)?
感谢关注!
setState
只关心组件状态一致性,不关心server/client一致性。因此 setState
不保证组件状态与其他任何状态一致。
之所以提供更新函数,是因为状态更新有时会延迟,并且不会在调用 setState
时立即发生。因此,如果没有 updater 函数,您基本上会遇到竞争条件。例如:
- 您的组件以
state = {counter: 0}
开头
- 您有一个按以下方式单击时更新计数器的按钮:
this.setState({counter: this.state.counter +1})
- 用户点击按钮的速度非常快,因此状态没有时间在点击之间更新。
- 这意味着计数器只会增加 1,而不是预期的 2 - 假设计数器最初为 0,两次单击按钮,调用最终为
this.setState({counter: 0+1})
,设置状态两次都为 1。
更新程序功能修复了这个问题,因为更新是按顺序应用的:
- 您的组件以
state = {counter: 0}
开头
- 您有一个按以下方式单击时更新计数器的按钮:
this.setState((currentState, props) => ({counter: currentState.counter + 1}))
- 用户点击按钮的速度非常快,因此状态没有时间在点击之间更新。
- 与其他方式不同,
currentState.counter + 1
不会立即得到评估 - 使用初始状态
{counter: 0}
调用第一个更新程序函数,并将状态设置为{counter: 0+1}
- 使用状态
{counter: 1}
调用第二个更新程序函数,并将状态设置为{counter: 1+1}
一般来说,updater 函数是较少 error-prone 改变状态的方式,很少有理由不使用它(尽管如果你设置的是静态值,你并不严格需要它)。
然而,您关心的是对状态的更新不会导致不正确的数据(重复等)。在那种情况下,我会注意更新的设计,以便它们是幂等的,并且无论数据的当前状态如何都可以工作。例如,不是使用数组来保存消息集合,而是使用映射,并通过该消息唯一的键或哈希存储每条消息,无论它来自哪里(毫秒时间戳可能足够唯一) .那么,当你从两个位置获取相同的数据时,就不会造成重复。
无论如何我都不是 React 方面的专家,而且只做了两个月,但这是我从我在 React 中的第一个项目中学到的东西,它就像随机引用一样简单。
如果您需要在使用 setState 后立即使用更新状态,请始终使用 updater 函数。让我举一个例子。
//
handleClick = () => {
//get a random color
const newColor = this.selectRandomColor();
//Set the state to this new color
this.setState({color:newColor});
//Change the background or some elements color to this new Color
this.changeBackgroundColor();
}
我这样做了,发生的事情是设置到正文的颜色始终是以前的颜色,而不是状态中的当前颜色,因为如您所知,setState
是批处理的。它发生在 React 认为最好执行它的时候。它不会立即执行。所以要解决这个问题,我所要做的就是将 this.changeColor
作为第二个参数传递给 setState。因为这确保了我应用的颜色与当前状态保持同步。
所以回答你的问题,在你的情况下,因为你的工作是在新消息到达时立即向用户显示消息,即使用更新状态,始终使用更新程序功能而不是目的。