簡體   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