setState在更新回调中的实际用法,而不是在React JS中传递对象

时间:2019-07-11 20:00:51

标签: javascript reactjs setstate

React文档中关于setState的内容如下:

If you need to set the state based on the previous state, read about the updater argument below

除了以下我不理解的句子:

  

如果正在使用可变对象,并且无法在 shouldComponentUpdate()中实现条件渲染逻辑,则仅当新状态不同于先前状态时才调用 setState()避免不必要的重新渲染。

他们说:

  

第一个参数是带有(state,props)=> stateChange 签名的 updater 函数... state 是对应用更改时的组件状态。

并举一个例子:

this.setState((state, props) => {
  return {counter: state.counter + props.step};
});

说:

  

保证更新程序功能收到的状态道具都是。更新器的输出与state进行浅层合并。

保证是最新的是什么意思,以及在决定是否应将setState与更新程序功能(state, props) => stateChange一起使用时应注意什么?还是直接以对象作为第一个参数?

让我们假设一个现实世界的场景。假设我们有一个精美的聊天应用程序,其中:

  1. 聊天状态由this.state = { messages: [] }表示;
  2. 先前的消息已加载并发出AJAX请求,并被添加到当前处于该状态的messages
  3. 如果其他用户(不是当前用户)向当前用户发送消息,则新消息将从实时WebSocket连接到达当前用户,并附加到当前处于状态的messages上。
  4. 如果当前用户是发送消息的人,则在消息发送完成后,一旦触发AJAX请求,消息就会被附加到状态3的messages处;
  5. li>

让我们假装这是我们的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?我应该考虑什么?我应该始终使用updater的{​​{1}}函数(为什么?)还是可以安全地直接将对象作为setState参数(为什么?)传递?

感谢您的关注!

2 个答案:

答案 0 :(得分:2)

setState只关心 component 状态的一致性,而不关心服务器/客户端的一致性。因此setState不保证组件状态与其他任何状态保持一致。

提供更新程序功能的原因是因为状态更新有时会延迟,并且在调用setState时不会立即发生。因此,如果没有更新程序功能,则您实际上具有竞争条件。例如:

  • 您的组件以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}

通常来说,更新程序功能是一种不太容易出错的更改状态的方法,很少有理由不使用它(尽管如果您设置的是静态值,则不一定要使用它)。

但是,您关心的是状态更新不会导致数据不正确(重复等)。在这种情况下,我会注意设计更新,使其无论数据的当前状态如何都是幂等且有效。例如,不要使用数组来保留消息的集合,而应使用映射,并通过该消息唯一的键或哈希存储每个消息,无论消息来自何处(毫秒时间戳可能足够唯一) 。然后,当您从两个位置获取相同的数据时,不会导致重复。

答案 1 :(得分:0)

我无论如何都不是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的第二个参数传递。因为这样可以确保我应用的颜色保持最新状态。

因此,要回答您的问题,因为您的工作是在收到新消息后立即将消息显示给用户,即使用UPDATED STATE,请始终使用updater函数而不是对象。