Real world usage of setState with an updater callback instead of passing an object in React JS

时光总嘲笑我的痴心妄想 提交于 2020-01-02 07:14:09

问题


The React documentation says the following about setState:

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

Beside the following sentence, which I don't understand:

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.

They say:

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.

And make an example:

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

Saying:

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.

What do they mean by guaranteed to be up-to-date and what should we be aware of when deciding if we should use setState with an updater function (state, props) => stateChange or directly with an object as the first parameter?

Let's assume a real world scenario. Suppose we have a fancy chat application where:

  1. The state of the chat is represented by this.state = { messages: [] };
  2. Previous messages are loaded making an AJAX request and are prepended to the messages currently in the state;
  3. If other users (not the current user) send a message to the current user, new messages arrive to current user from a realtime WebSocket connection and are appended to the messages currently in the state;
  4. If it is the current user the one who sends the message, the message is appended to the messages of the state as in point 3 as soon as the AJAX request fired when the message is sent completes;

Let's pretend this is our FancyChat component:

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>
        )
    }

}

With so many asynchronous operations, how can I be really sure that this.state.messages will always be consistent with the data on the server and how would I use setState for each case? What considerations I should make? Should I always use the updater function of setState (why?) or is safe to directly pass an object as the updater parameter (why?)?

Thank you for the attention!


回答1:


setState is only concerned with component state consistency, not server/client consistency. So setState makes no guarantees that the component state is consistent with anything else.

The reason an updater function is provided, is because state updates are sometimes delayed, and don't occur immediately when setState is called. Therefore, without the updater function, you have essentially a race condition. For example:

  • your component begins with state = {counter: 0}
  • you have a button that updates the counter when clicked in the following way: this.setState({counter: this.state.counter +1})
  • the user clicks the button really fast, so that the state does not have time to be updated between clicks.
  • that means that the counter will only increase by one, instead of the expected 2 - assuming that counter was originally 0, both times the button is clicked, the call ends up being this.setState({counter: 0+1}), setting the state to 1 both times.

An updater function fixes this, because the updates are applied in order:

  • your component begins with state = {counter: 0}
  • you have a button that updates the counter when clicked in the following way: this.setState((currentState, props) => ({counter: currentState.counter + 1}))
  • the user clicks the button really fast, so that the state does not have time to be updated between clicks.
  • unlike the other way, currentState.counter + 1 does not get evaluated immediately
  • the first updater function is called with the initial state {counter: 0}, and sets the state to {counter: 0+1}
  • the second updater function is called with the state {counter: 1}, and sets the state to {counter: 1+1}

Generally speaking, the updater function is the less error-prone way to change the state, and there is rarely a reason to not use it (although if you are setting a static value, you don't strictly need it).

What you care about, however, is that updates to the state don't cause improper data (duplicates and the like). In that case, I would take care that the updates are designed so that they are idempotent and work no matter the current state of the data. For instance, instead of using an array to keep the collection of messages, use a map instead, and store each message by key or hash that is unique to that message, no matter where it came from (a millisecond timestamp may be unique enough). Then, when you get the same data from two locations, it will not cause duplicates.




回答2:


I'm not an expert in React by any means and have only been doing it for two months only, but here's what I learned from my very first project in React, which was as simple as showing a random quote.

If you need to use the updated state right after you use setState, always use the updater function. Let me give you an example.

// 
 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();
}

I did this and what happened was that the color that was set to the body was always the previous color and not the current color in the state, because as you know, the setState is batched. It happens when React thinks it's best to execute it. It's not executed immediately. So to solve this problem, all I have to do was pass this.changeColor as a second argument to setState. Because that ensured that the color I applied was kept up to date with the current state.

So to answer your question, in your case, since you're job is to display the message to the user as soon as a new message arrives, i.e use the UPDATED STATE, always use the updater function and not the object.



来源:https://stackoverflow.com/questions/56996496/real-world-usage-of-setstate-with-an-updater-callback-instead-of-passing-an-obje

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!