繁体   English   中英

使用更新程序回调实际使用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

除了以下句子,我不明白:

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

他们说:

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

举个例子:

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

他说:

更新程序功能接收的状态道具保证是最新的 updater的输出与state轻微合并。

在确定是否应该使用带有更新程序功能的setState (state, props) => stateChange或直接将对象作为第一个参数时,它们是什么意思保证是最新的 ?我们应该注意什么?

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

  1. 聊天状态由this.state = { messages: [] } ;
  2. 先前的消息被加载,发出一个AJAX请求,并被添加到当前状态的messages ;
  3. 如果其他用户(不是当前用户)向当前用户发送消息,则新消息从实时WebSocket连接到达当前用户,并附加到当前状态的messages中;
  4. 如果当前用户是发送消息的用户,则当消息发送完成时,一旦发出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仅关注组件状态一致性,而不关心服务器/客户端一致性。 因此, 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}调用第一个updater函数,并将状态设置为{counter: 0+1}
  • 使用状态{counter: 1}调用第二个updater函数,并将状态设置为{counter: 1+1}

一般来说,updater函数是更不容易出错的改变状态的方法,很少有理由不使用它(尽管如果你设置的是静态值,你并不严格需要它)。

但是,您关心的是对状态的更新不会导致不正确的数据(重复等)。 在这种情况下,我会注意更新的设计使它们无论数据的当前状态如何都是幂等的并且可以工作。 例如,不是使用数组来保留消息集合,而是使用映射,并按照该消息所特有的键或散列存储每条消息,无论它来自何处(毫秒时间戳可能足够独特) 。 然后,当您从两个位置获得相同的数据时,它不会导致重复。

我无论如何都不是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函数而不是对象。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM