Недавнее обновление 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