React v16.4.0: вам может не понадобиться производное состояние

внешний интерфейс Безопасность React.js

Очень долгое время,componentWillReceivePropsЖизненные циклы — единственный способ реагировать на изменения свойств и состояния обновления без дополнительных рендеров. В версии 16.3,Мы представляем новый альтернативный жизненный цикл getDerivedStateFromProps.чтобы решить ту же проблему более безопасно. В то же время мы поняли, что люди неправильно понимают оба метода, и обнаружили, что некоторые из этих контрпримеров вызывают странные ошибки. В этой версии мы это исправили,и сделать производное состояние более предсказуемым, чтобы нам было легче заметить последствия злоупотреблений.

Пример счетчика в этой статье содержит как старый метод componentWillReceiveProps, так и новый метод getDerivedStateFromProps.

Когда использовать производное состояние

getDerivedStateFromPropsсуществует только для одной цели. Это позволяет компоненту обновлять свое внутреннее состояние в ответ на изменения в свойствах. как мы упоминали ранееЗапишите текущее направление прокрутки в соответствии с измененным свойством смещения.илиЗагружать дополнительные данные на основе исходного свойства.

Мы приводим много примеров, потому что, как правило,Производное состояние следует использовать с осторожностью.我们见过的所有关于派生状态的问题最后都可以被归为两种:(1)从props那里无条件地更新state(2)当props和state不匹配的时候更新state(我们在下面会深入探讨)

  • Если вы используете производное состояние для запоминания результатов операций на основе текущих свойств, оно вам не нужно. Смотри ниже关于缓存记忆(memoization).
  • Если вы находитесь во втором случае, то ваши компоненты, вероятно, слишком часто сбрасываются. Читайте дальше.

Распространенные ошибки при использовании производного состояния

"контролируемый"а также«Бесконтрольный»Обычно относится к элементу управления вводом для формы, но его также можно использовать для описания того, где находятся данные компонента. Данные, переданные через реквизиты, могут быть вызваныконтролируемый(поскольку родительский компонент контролирует эти данные). Данные, которые существуют только во внутреннем состоянии, называютсянеконтролируемый(потому что родительский компонент не может изменить его напрямую).

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

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

Пример счетчика: безусловное копирование реквизита в состояние

Распространенным заблуждением является то, чтоgetDerivedStateFromPropsа такжеcomponentWillReceivePropsБудет вызываться только тогда, когда реквизит "изменится". Эти жизненные циклы будут вызываться при рендеринге любого родительского компонента, независимо от того, изменились ли реквизиты на самом деле. Поэтому используйте эти циклы длябезусловныйНебезопасно перезаписывать состояние.Это приведет к потере обновлений состояния..

Давайте продемонстрируем эту проблему. Это邮件输入Компонент, который «отображает» атрибут электронной почты в состояние:

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  componentWillReceiveProps(nextProps) {
    // 这样会抹去所有的内部状态更新!
    // 不要这样做.
    this.setState({ email: nextProps.email });
  }
}

Похоже, этот компонент в порядке. состояние инициализируется со значениями в реквизитах, а затем с<input>обновляется с вводом. Но если родительский компонент отрисовывается, мы находимся в<input>Все, что вы наберете, исчезнет! (посмотреть демо здесь) даже если мы сравним перед сбросомnextProps.email !== this.state.emailТак будет.

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

Надеюсь, теперь мы можем понять, почемуБезусловное копирование реквизита в состояние — плохая идея.. Прежде чем рассматривать возможные решения, давайте рассмотрим связанный пример: что, если мы будем обновлять состояние только при изменении свойства электронной почты?

Пример счетчика: перезаписать состояние при изменении реквизита

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

class EmailInput extends Component {
  state = {
    email: this.props.email
  };

  componentWillReceiveProps(nextProps) {
    // 只要props.email改变, 更新state.
    if (nextProps.email !== this.props.email) {
      this.setState({
        email: nextProps.email
      });
    }
  }
  
  // ...
}

Хотя в приведенном выше примере используетсяcomponentWillReceiveProps, но и этот контрпример веренgetDerivedStateFromPropsБыть применимым

Мы просто сделали большой шаг вперед. Теперь наш компонент будет перезаписывать то, что мы печатаем, только когда свойства действительно меняются.

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

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

лучшее решение

Рекомендуется: полностью контролируемые компоненты

Решение вышеуказанной проблемы состоит в том, чтобы полностью удалить состояние из наших компонентов. Если электронная почта существует только как свойство, нам не нужно беспокоиться о конфликтах с состоянием. мы можем даже поставитьEmailInputКак более легкий функциональный компонент:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

Это упрощает реализацию нашего компонента, но если мы все-таки хотим сохранить черновое значение ввода, то родительскому компоненту нужно сделать это вручную,Увидеть демонстрацию

Рекомендуется: Полностью неуправляемые компоненты, идентифицированные ключом

Другой вариант — чтобы наш компонент полностью контролировал состояние «черновик» eamil. Здесь наш компонент по-прежнему принимает свойства в свойствах в качестве начальных значений, но игнорирует последующие изменения свойств.

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}

Чтобы сбросить входное значение при переключении между разными вещами (например, упомянутым выше менеджером паролей), мы можем использовать React’skeyАтрибуты. когдаkeyизменится, React будетвоссоздать новый компонент вместо его обновления. Ключи обычно используются в динамических списках, но они также полезны и здесь. Здесь мы используем идентификатор пользователя для воссоздания компонента ввода электронной почты при выборе нового пользователя:

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

Всякий раз, когда идентификатор меняется,EmailInputбудет воссоздан, и состояние будет сброшено до последнегоdefaultEmailстоимость. (Нажмите здесь, чтобы просмотреть демонстрацию) на самом деле вам не нужно добавлять ключ в каждое поле ввода. добавить один ко всей формеkeyокажется более полезным. Всякий раз, когда ключ изменяется, все компоненты формы перестраиваются и получают чистые начальные значения.

В большинстве случаев это лучший способ сбросить состояние.

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

Вариант 1: сброс неуправляемого компонента через свойство ID

Если его нельзя использовать по какой-либо причинеkey(например, инициализация компонентов стоит дорого), выполнимое, но громоздкое решение состоит в том, чтобыgetDerivedStateFromPropsСлушайте изменения в «userID».

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };

  static getDerivedStateFromProps(props, state) {
    // Any time the current user changes,
    // Reset any parts of state that are tied to that user.
    // In this simple example, that's just the email.
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}

Это также обеспечивает гибкость для сброса внутреннего состояния виджета только по нашему выбору (Здесь, чтобы увидеть демо)

Приведенный выше пример одинаково для компонентаWillReceiveProops

Альтернатива 2. Используйте метод экземпляра для сброса неконтролируемого компонента.

В редких случаях может потребоваться сброс состояния без подходящего идентификатора в качестве ключа. Один из способов — установить для ключа случайное или увеличенное значение и изменить его, когда вы захотите выполнить сброс. Другое возможное решение — открыть метод экземпляра для императивного изменения внутреннего состояния:

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail
  };

  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }

  // ...
}

компонент родительской формыВызовите этот метод по ссылке, (Нажмите здесь, чтобы просмотреть демонстрацию)

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

О кэш-памяти (мемоизации)

Мы также видели использование производного состояния для обеспеченияrenderВычислительно затратные значения в пересчитываются только при изменении ввода. Эта техника называетсяmemoization.

Использование его для кэш-памяти не обязательно плохо, но обычно это не лучшее решение. Существует некоторая сложность в управлении производным состоянием, которая увеличивается с дополнительными свойствами. Например, если мы добавляем в компонент второе производное поле, то нам нужно отслеживать изменения этих двух полей отдельно.

Давайте рассмотрим пример, в котором мы передаем список компоненту, и ему необходимо отфильтровать данные, введенные пользователем, чтобы отобразить совпадающие элементы. Мы можем использовать производное состояние для сохранения отфильтрованного списка.

class Example extends Component {
  state = {
    filterText: "",
  };

  // *******************************************************
  // NOTE: 这个例子不是我们推荐的做法
  // 推荐的方法参见下面的例子.
  // *******************************************************

  static getDerivedStateFromProps(props, state) {
    // 每当列表数组或关键字变化时筛选列表.
    // 注意到我们需要储存prevPropsList和prevFilterText来监听变化.
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

Реализация здесь позволяет избежатьfilteredListНенужный перерасчет. Но это слишком сложно, потому что нужно отслеживать и прослушивать изменения реквизитов и состояния отдельно, чтобы правильно обновлять список фильтров. В этом примере мы можем использоватьPureComponentчтобы упростить операцию и поместить операцию фильтра в метод рендеринга:

// PureComponents只会在至少state和props中有一个属性发生变化时渲染.
// 变化是通过引用比较来判断的.
class Example extends PureComponent {
  // State only needs to hold the current filter text value:
  state = {
    filterText: ""
  };

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 只有props.list 或 state.filterText 改变时才会调用.
    const filteredList = this.props.list.filter(
      item => item.text.includes(this.state.filterText)
    )

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

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

import memoize from "memoize-one";

class Example extends Component {
  // State只需要去维护目前的筛选关键字:
  state = { filterText: "" };

  // 当列表数组或关键字变化时重新筛选
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 计算渲染列表时,如果参数同上次计算没有改变,`memoize-one`会复用上次返回的结果
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

Это упрощает достижение той же функциональности, что и производное состояние!

Суммировать

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

getDerivedStateFromPropsИспользование стоит переосмыслить, потому что это расширенная функция с определенной сложностью, и мы должны использовать ее с осторожностью.

Оригинальная ссылка:You Probably Don't Need Derived State