Сохраняет ли React порядок обновлений состояния?

140

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

  1. тот же компонент?
  2. разные компоненты?

Рассмотрите возможность нажатия кнопки в следующих примерах:

1. Существует ли вероятность, что a ложно, а b истинно для:

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false, b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.setState({ a: true });
    this.setState({ b: true });
  }
}

2. Существует ли вероятность, что a ложно, а b истинно для:

class SuperContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false };
  }

  render() {
    return <Container setParentState={this.setState.bind(this)}/>
  }
}

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.props.setParentState({ a: true });
    this.setState({ b: true });
  }
}

Имейте в виду, что это крайние упрощения моего варианта использования. Я понимаю, что могу сделать это по-другому, например, обновить оба параметра состояния одновременно в примере 1, а также выполнить второе обновление состояния в обратном вызове к первому обновлению состояния в примере 2. Однако это не мой вопрос, и меня интересует только наличие четко определенного способа, которым React выполняет эти обновления состояния, и ничего больше.

Мы очень ценим любой ответ, подкрепленный документацией.

darksmurf
источник
1
Смотрите это: stackoverflow.com/a/36087156/3776299
helb 01
3
это не кажется бессмысленным вопросом, вы также можете задать этот вопрос по вопросам github на странице реакции, дан Абрамов обычно очень помогает там. Когда у меня возникали такие сложные вопросы, я задавал, и он отвечал. Плохо то, что такие проблемы не публикуются публично, как в официальных документах (чтобы другие могли легко получить к ним доступ). Я также чувствую, что в официальных документах React не хватает подробного освещения некоторых тем, таких как тема из вашего вопроса и т. Д.
giorgim 01
Например, возьмите это: github.com/facebook/react/issues/11793 , я считаю, что материалы, обсуждаемые в этом выпуске, будут полезны для многих разработчиков, но этого нет в официальных документах, потому что люди из FB считают это продвинутым. Возможно, то же самое и о других вещах. Я бы подумал, что официальная статья, озаглавленная что-то вроде «государственное управление в глубокой реакции» или «подводные камни государственного управления», в которой рассматриваются все угловые случаи государственного управления, как в вашем вопросе, была бы неплохой. возможно, мы сможем подтолкнуть разработчиков FB к расширению документации такими вещами :)
giorgim 01
В моем вопросе есть ссылка на отличную статью о среде. Он должен охватывать 95% случаев использования в штатах. :)
Михал
2
@Michal, но эта статья до сих пор не отвечает на этот вопрос ИМХО
giorgim 01

Ответы:

336

Я работаю над React.

TLDR:

Но можете ли вы доверять React в обновлении состояния в том же порядке, в котором вызывается setState?

  • тот же компонент?

Да.

  • разные компоненты?

Да.

Порядок обновлений всегда соблюдается. Видите ли вы промежуточное состояние «между ними» или нет, зависит от того, находитесь вы внутри партии или нет.

В настоящее время (React 16 и более ранние версии) по умолчанию пакетируются только обновления внутри обработчиков событий React . Существует нестабильный API для принудительной пакетной обработки вне обработчиков событий в редких случаях, когда это необходимо.

В будущих версиях (вероятно, React 17 и новее) React будет пакетировать все обновления по умолчанию, поэтому вам не придется об этом думать. Как всегда, мы будем сообщать о любых изменениях по этому поводу в блоге React и в примечаниях к выпуску.


Ключ к пониманию этого заключается в том, что независимо от того, сколько setState()вызовов и сколько компонентов вы выполняете внутри обработчика событий React , они будут производить только один повторный рендеринг в конце события . Это очень важно для хорошей производительности в больших приложениях , потому что если Childи Parentкаждый вызов setState()при обработке события щелчка, вы не хотите , чтобы пересборка Childдважды.

В обоих ваших примерах setState()вызовы происходят внутри обработчика событий React. Поэтому они всегда сбрасываются вместе в конце события (и вы не видите промежуточное состояние).

Обновления всегда объединяются неглубоко в порядке их появления . Итак, если первое обновление есть {a: 10}, второе есть {b: 20}, а третье - {a: 30}состояние рендеринга будет {a: 30, b: 20}. Более недавнее обновление того же ключа состояния (например, как aв моем примере) всегда «выигрывает».

this.stateОбъект обновляется , когда мы вновь сделать пользовательский интерфейс в конце партии. Поэтому, если вам нужно обновить состояние на основе предыдущего состояния (например, увеличение счетчика), вы должны использовать функциональную setState(fn)версию, которая дает вам предыдущее состояние, вместо чтения из this.state. Если вам интересно, почему это происходит, я подробно объяснил это в этом комментарии .


В вашем примере мы не увидим «промежуточное состояние», потому что мы находимся внутри обработчика событий React, где включена пакетная обработка (потому что React «знает», когда мы выходим из этого события).

Однако как в React 16, так и в более ранних версиях пакетная обработка по умолчанию отсутствует за пределами обработчиков событий React . Итак, если бы в вашем примере у нас был обработчик ответа AJAX вместо handleClick, каждый setState()будет обрабатываться немедленно, как это происходит. В этом случае, да, вы бы видеть промежуточное состояние:

promise.then(() => {
  // We're not in an event handler, so these are flushed separately.
  this.setState({a: true}); // Re-renders with {a: true, b: false }
  this.setState({b: true}); // Re-renders with {a: true, b: true }
  this.props.setParentState(); // Re-renders the parent
});

Мы понимаем, что неудобно, что поведение зависит от того, работаете вы в обработчике событий или нет . Это изменится в будущей версии React, которая будет пакетировать все обновления по умолчанию (и предоставлять дополнительный API для синхронного сброса изменений). Пока мы не переключим поведение по умолчанию (возможно, в React 17), есть API, который вы можете использовать для принудительной пакетной обработки :

promise.then(() => {
  // Forces batching
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: true}); // Doesn't re-render yet
    this.setState({b: true}); // Doesn't re-render yet
    this.props.setParentState(); // Doesn't re-render yet
  });
  // When we exit unstable_batchedUpdates, re-renders once
});

Все внутренние обработчики событий React обернуты unstable_batchedUpdates, поэтому по умолчанию они сгруппированы. Обратите внимание, что двойная упаковка обновления не unstable_batchedUpdatesимеет никакого эффекта. Обновления сбрасываются, когда мы выходим из самого внешнего unstable_batchedUpdatesвызова.

Этот API «нестабилен» в том смысле, что мы удалим его, когда пакетная обработка уже включена по умолчанию. Однако мы не будем удалять его в младшей версии, поэтому вы можете смело полагаться на него до React 17, если вам нужно принудительно использовать пакетную обработку в некоторых случаях за пределами обработчиков событий React.


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

Дан Абрамов
источник
1
Один из способов «всегда получать правильный порядок» - это создать временный объект, присвоить ему разные значения (например, obj.a = true; obj.b = true), а затем в конце просто сделать this.setState(obj). Это безопасно независимо от того, находитесь ли вы внутри обработчика событий или нет. Это может быть изящный трюк, если вы часто делаете ошибку, задавая состояние несколько раз вне обработчиков событий.
Крис
Таким образом, мы не можем полагаться на то, что пакетная обработка будет ограничена одним обработчиком событий - как вы ясно дали понять, по крайней мере, потому, что это произойдет не скоро. Тогда мы должны использовать setState с функцией обновления, чтобы получить доступ к самому последнему состоянию, верно? Но что, если мне нужно использовать какой-нибудь state.filter, чтобы XHR читал некоторые данные, а затем переводил их в состояние? Похоже, мне придется поместить XHR с отложенным обратным вызовом (и, следовательно, побочным эффектом) в средство обновления. Так считается ли это лучшей практикой?
Максим Гумеров
1
И, кстати, это также означает, что нам вообще не следует читать из this.state; Единственный разумный способ прочитать некоторое состояние .X - это прочитать его в функции обновления, из его аргумента. И писать в this.state тоже небезопасно. Тогда зачем вообще разрешать доступ к this.state? Возможно, это должны быть отдельные вопросы, но в основном я просто пытаюсь понять, правильно ли я получил объяснение.
Максим Гумеров
10
этот ответ должен быть добавлен в документацию reactjs.org
Deen John
2
Не могли бы вы уточнить в этом посте, включает ли «Обработчик событий React» componentDidUpdateи другие обратные вызовы жизненного цикла, начиная с React 16? Заранее спасибо!
Иван
6

На самом деле это довольно интересный вопрос, но ответ не должен быть слишком сложным. Есть отличная статья о среде, в которой есть ответ.

1) Если вы сделаете это

this.setState({ a: true });
this.setState({ b: true });

Не думаю, что будет ситуация, когда aбудет trueи bбудет falseиз-за пакетирования .

Однако, если bзависит от,a то действительно может возникнуть ситуация, когда вы не получите ожидаемое состояние.

// assuming this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});

После обработки всех вышеуказанных вызовов this.state.valueбудет 1, а не 3, как вы ожидали.

Об этом говорится в статье: setState accepts a function as its parameter

// assuming this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));

Это даст нам this.state.value === 3

Michal
источник
Что делать , если this.state.valueобновляется и обработчики событий (где setStateдозируется) и AJAX обратных вызовов (где setStateэто не дозируется). В обработчиках событий я бы использовал, updater functionчтобы всегда быть уверенным, что я обновляю состояние, используя текущее обновленное состояние, предоставляемое функцией. Должен ли я использовать setStateфункцию обновления внутри кода обратного вызова AJAX, даже если я знаю, что он не пакетируется? Не могли бы вы прояснить использование setStateв обратном вызове AJAX с или без использования функции обновления? Спасибо!
tonix
@Michal, привет, Михал просто хотел задать простой вопрос, правда ли, что если у нас есть this.setState ({value: 0}); this.setState ({значение: this.state.value + 1}); первый setState будет проигнорирован и будет выполнен только второй setState?
Диккенс
@ Диккенс Я считаю, что оба setStateбудут казнены, но победит последний.
Михал
3

Несколько вызовов в течение одного цикла могут быть объединены в пакет. Например, если вы попытаетесь увеличить количество товара более одного раза в одном и том же цикле, это приведет к эквиваленту:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

https://reactjs.org/docs/react-component.html

Mosè Raguzzini
источник
3

как в документе

setState () ставит в очередь изменения состояния компонента и сообщает React, что этот компонент и его дочерние элементы должны быть повторно отрисованы с обновленным состоянием. Это основной метод, который вы используете для обновления пользовательского интерфейса в ответ на обработчики событий и ответы сервера.

он выполнит изменение, как в очереди ( FIFO : First In First Out), первый вызов будет первым для предварительной обработки

Али
источник
привет Али, просто хотел задать простой вопрос, правда ли, что если у нас есть this.setState ({value: 0}); this.setState ({значение: this.state.value + 1}); первый setState будет проигнорирован и будет выполнен только второй setState?
Диккенс