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 还是直接用对象作为第一个参数?

让我们假设一个真实的场景。假设我们有一个花哨的聊天应用程序,其中:

  1. 聊天状态由this.state = { messages: [] };
  2. 表示
  3. 先前的消息已加载,发出 AJAX 请求,并被添加到当前处于该状态的 messages 之前;
  4. 如果其他用户(不是当前用户)向当前用户发送消息,新消息将从实时 WebSocket 连接到达当前用户,并附加到当前状态的 messages
  5. 如果发送消息的是当前用户,则当 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?我应该考虑什么?我应该始终使用 setStateupdater 函数(为什么?)还是直接将对象作为 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。因为这确保了我应用的颜色与当前状态保持同步。

所以回答你的问题,在你的情况下,因为你的工作是在新消息到达时立即向用户显示消息,即使用更新状态,始终使用更新程序功能而不是目的。