Недавнее обновление React, 16.4, изменило принцип работы getDerivedStateFromProps. Основное отличие состоит в том, что теперь он вызывается даже при state изменениях, а не, как следует из названия, только при prop изменениях - так это работало в версии 16.3.

Если вы не знаете о getDerivedStateFromProps, это static метод жизненного цикла, представленный в React 16.3 для подготовки к асинхронной визуализации.

В 16.3 он был предложен в качестве альтернативы для componentWillReceiveProps, который устарел и будет удален в React 17.

getDerivedStateFromProps добавляется как более безопасная альтернатива устаревшему componentWillReceiveProps. - Реагировать 16.3

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

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

Рассмотрим следующий пример. У вас есть Page компонент, который отображает текст текущей страницы и позволяет вам редактировать его. Пока что Page может быть неконтролируемым - мы сохраняем text в state и обновляем его только при нажатии кнопки на странице, вызывая обновление родительского компонента.

Теперь давайте добавим разбиение на страницы: у родительского компонента есть кнопка, позволяющая перейти на следующую страницу, которая повторно отобразит ваш Page компонент с новым text prop. Теперь это должно отбросить локальное состояние в компоненте Page и вместо этого отобразить текст новой страницы.

Вот Codeandbox приложения:

App:

class App extends React.Component {
  state = {
    pages: ["Hello from Page 0", "Hello from Page 1", "Hello from Page 2"],
    currentPage: 0
  };

  onNextPage = () => {
    this.setState({
      currentPage: (this.state.currentPage + 1) % this.state.pages.length
    });
  };

  onUpdate = value => {
    const { pages, currentPage } = this.state;
    this.setState(
      {
        pages: [
          ...pages.slice(0, currentPage),
          value,
          ...pages.slice(currentPage + 1)
        ]
      }
    );
  };

  render() {
    const currentPageText = this.state.pages[this.state.currentPage];
    return (
      <div style={styles}>
        <Page value={currentPageText} onUpdate={this.onUpdate} />
        <button onClick={this.onNextPage}>Next Page</button>
      </div>
    );
  }
}

И вот первая попытка реализовать компонент Page:

import React from "react";

export default class Page extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.value,
    };
  }

  componentWillReceiveProps(nextProps) {
    // if new value props was received, overwrite state
    // happens f.i. when changing pages
      this.setState({
        value: nextProps.value
      });
  }

  // ALTERNATIVE: using getDerivedStateFromProps
  static getDerivedStateFromProps(props, state) {
      return {
        value: props.value
      };
  }

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

  onSave = () => {
    this.props.onUpdate(this.state.value);
  };

  render() {
    return (
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center"
        }}
      >
        <textarea value={this.state.value} onChange={this.onChange} />
        <button onClick={this.onSave}>Save</button>
      </div>
    );
  }
}

Ошибка

Здесь важно отметить использование componentWillReceiveProps или getDerivedStateFromProps для обновления локального text состояния при изменении страницы.

Но прямо сейчас в компоненте Page есть ошибка (даже если она незаметна в том, как сейчас используется). Мы сбрасываем состояние при каждой повторной визуализации. Это из-за того, как работал componentWillReceiveProps / getStateDerivedFromProps:

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

А теперь самое интересное: имея этот (ошибочный?) Код, который отлично работал в нашем примере приложения, больше не работает в React 16.4, когда вы используете getDerivedStateFromProps: onChange на textarea триггерах setState, который сам запускает getDerivedStateFromProps, который снова устанавливает состояние на старое text из props. Это означает, что вы больше не можете писать в textarea.

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

Решение

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

Для componentWillReceiveProps это было легко:

componentWillReceiveProps(nextProps) {
    // if new value props was received, overwrite state
    // happens f.i. when changing pages
    if (nextProps.value !== this.props.value) {
      this.setState({
        value: nextProps.value
      });
    }
  }

Но с static getDerivedStateFromProps не все так просто: он статичен, и мы получаем только (props, state) в качестве аргументов. Чтобы сравнить props с prevProps, мы должны сохранить prevProps в state, чтобы иметь к нему доступ.

constructor(props) {
    super(props);
    this.state = {
      prevProps: props,
      value: props.value,
    };
  }

  static getDerivedStateFromProps(props, state) {
    // comment this "if" and see the component break
    if (props.value !== state.prevProps.value) {
      return {
        prevProps: props,
        value: props.value
      };
    }
  }

Это уродливо

А теперь сделай шаг назад. Подумайте о простоте приложения и о том, чего мы пытаемся достичь: текстовое поле. А затем снова посмотрите на код решения.
Очевидно, что-то не так с React, когда это рекомендуемый способ обработки такого фундаментального варианта использования в React 16.4. Мне кажется хакерским сохранять предыдущий props в state. Должен быть более простой способ сделать это. (Вы также можете (ab) использовать атрибут key и выполнить полное перемонтирование компонента Page, избегая getDerivedStateFromProps. Это описано в другой статье или этой. Но это тоже не кажется отточенным.)

Я искренне надеюсь, что инициатива команды React по продвижению асинхронного рендеринга не пойдет дальше за счет удобства использования React в повседневных сценариях, подобных описанному выше. React 16.4 кажется шагом назад после стольких замечательных и полезных функций в React 16.3. После удаления componentWillReceiveProps больше не будет простого способа просто слушать props изменения или получать доступ к предыдущему props.

Первоначально опубликовано на cmichel.io