Реальное использование setState с обратным вызовом средства обновления вместо передачи объекта в React JS - PullRequest
3 голосов
/ 11 июля 2019

В документации 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 ... состояние является ссылкой на состояние компонента во время применения изменения.

И приведем пример:

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

Говоря:

Оба состояния и реквизитов , полученных функцией обновления, гарантированно будут актуальными.Вывод средства обновления поверхностно объединен с состоянием.

Что они означают под гарантированно актуальными и что мы должны знать при принятии решения, если мыследует использовать setState с функцией обновления (state, props) => stateChange или напрямую с объектом в качестве первого параметра?

Давайте предположим, что сценарий реального мира.Предположим, у нас есть необычное приложение для чата, в котором:

  1. Состояние чата представлено this.state = { messages: [] };
  2. Загружаются предыдущие сообщения с запросом AJAX и добавляются к messages в данный момент в состоянии;
  3. Если другие пользователи (не текущий пользователь) отправляют сообщение текущему пользователю, новые сообщения поступают текущему пользователю из соединения WebSocket в реальном времени и добавляются к messages в настоящее времяв состоянии;
  4. Если отправителем сообщения является текущий пользователь, сообщение добавляется в messages состояния, как в пункте 3, как только запрос AJAX сработал, когда сообщениеотправлено завершено;

Давайте представим, что это наш 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 функцию setState (почему?) Или безопасно передать объект в качестве параметра updater (почему?)?

Спасибо за внимание!

Ответы [ 2 ]

2 голосов
/ 11 июля 2019

setState касается только компонента согласованности состояний, а не согласованности сервера / клиента.Таким образом, setState не дает никаких гарантий того, что состояние компонента соответствует чему-либо еще.

Причина, по которой предоставляется функция обновления, заключается в том, что обновления состояния иногда задерживаются и не происходят немедленно, когда setStateназывается.Следовательно, без функции обновления у вас, по сути, будет состояние гонки.Например:

  • ваш компонент начинается с state = {counter: 0}
  • , у вас есть кнопка, которая обновляет счетчик при нажатии следующим образом: this.setState({counter: this.state.counter +1})
  • пользователь нажимает кнопку очень быстро, так что состояние не успевает обновляться между щелчками.
  • , что означает, что счетчик будет увеличиваться только на единицу вместо ожидаемых 2 - при условии, что счетчик изначально был 0, при двойном нажатии кнопки вызов заканчивается на this.setState({counter: 0+1}), оба состояния устанавливаются равными 1.

Функция обновления исправляет это, поскольку обновления применяются по порядку:

  • ваш компонент начинается с state = {counter: 0}
  • у вас есть кнопка, которая обновляет счетчик при нажатии следующим образом: this.setState((currentState, props) => ({counter: currentState.counter + 1}))
  • пользователь действительно нажимает кнопкубыстро, так что состояние не успевает обновляться между щелчками.
  • в отличие от другого способа, currentState.counter + 1 не оценивается сразу
  • первая функция обновления называется calсветодиод с начальным состоянием {counter: 0} и устанавливает состояние {counter: 0+1}
  • , вторая функция средства обновления вызывается с состоянием {counter: 1} и устанавливает состояние {counter: 1+1}

Вообще говоря, функция обновления является менее подверженным ошибкам способом изменения состояния, и редко есть причина не использовать его (хотя, если вы устанавливаете статическое значение, оно вам строго не нужно).

Однако вас волнует, что обновления состояния не приводят к неправильным данным (дубликатам и т. П.).В этом случае я бы позаботился о том, чтобы обновления разрабатывались так, чтобы они были идемпотентными и работали независимо от текущего состояния данных.Например, вместо использования массива для хранения коллекции сообщений, вместо этого используйте карту и сохраняйте каждое сообщение по ключу или хешу, который уникален для этого сообщения, независимо от того, откуда оно пришло (отметка времени в миллисекундах может быть достаточно уникальной),Затем, когда вы получите одинаковые данные из двух мест, они не вызовут дубликатов.

0 голосов
/ 11 июля 2019

Я ни в коем случае не являюсь экспертом в React и делаю это только два месяца, но вот что я узнал из своего самого первого проекта в React, который был так же прост, как показ случайной цитаты.

Если вам нужно использовать обновленное состояние сразу после использования setState, всегда используйте функцию обновления.Позвольте привести пример.

// 
 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.Потому что это гарантировало, что примененный мною цвет был актуален с текущим состоянием.

Итак, чтобы ответить на ваш вопрос, в вашем случае, так как ваша задача - показать сообщение пользователю, как толькоприходит новое сообщение, т.е. используйте ОБНОВЛЕННОЕ СОСТОЯНИЕ, всегда используйте функцию обновления, а не объект.

...