Привет, друзья!
Итак, разработчики Реакта решили сделать нашу работу с их либой более линейной, направить, так сказать, нас нерадивых на путь наименьшего шанса ошибиться и написать плохой код, что, на мой взгляд, является нашим неотъемлемым правом и способом совершенствоваться и изобретать. Речь идет о всеми любимых методах componentWillReceiveProps и других из той же серии, их больше не будет, но нам дадут альтернативу в виде статического метода getDerivedStateFromProps. Лично мне он напоминает темную комнату, где лежат вещи, и их нужно найти, но ничего не видно.
Разработчики в своих ответах на гневные комментарии пользователей Реакта пишут мол: Ну не дадим мы вам prevProps, это невозможно, придумайте что-нибудь, prevProps нет, ну вы держитесь там, просто кешируйте их в состоянии, в общем предлагают нам сделать небольшой костылек в нашем новом хорошем коде. Это все конечно несложно, можно понять и простить, но вот меня раздосадовал тот факт, что теперь у меня нет контекста this, комнату мою замуровали, из нее ничего не видно, даже соседей не слышно, вот и решил я написать для себя штуку, которая скроет в себе все костыли и мой код будет с виду хоть и странным, но бескостыльным (а бескостыльным ли?).
В общем, мне нужно внедрить prevProps в состояние компонента, еще хочется чтобы все выглядело как обычно, а также невозможно прожить без волшебного this в статическом getDerivedStateFromProps (вот дурак!). Два дня мучений и самосовершенствования и все готово, я родил мышь.
Установка
npm install --save state-master
Использование
Просто пишем такие же getDerivedStateFromProps и componentDidUpdate, но уже модифицированные.
Оборачиваем наш компонент в "withStateMaster", передаем туда список "пропсов", изменения которых нужно отслеживать
import {Component} from 'react'
import {withStateMaster, registerContext, unregisterContext} from 'state-master';
// Список "пропсов", изменения которых нужно отслеживать
const PROP_LIST = ['width', 'height', 'bgColor', 'fontSize', 'autoSize'];
// или просто строка, если только одно значение
const PROP_LIST = 'value';
// добавление начального состояния опционально
const INITIAL_STATE = {
width: 1000,
height: 500
};
class ContainerComponent extends Component {
static displayName = 'Container';
static getDerivedStateFromProps(data) {
const {
nextProps,
prevProps,
state,
isInitial,
changed,
changedProps,
isChanged,
add,
addIfChanged,
isChangedAny,
addIfChangedAny,
isChangedAll,
call,
get
} = data;
// ниже пойдет речь об изменившихся пропсах, это только те, которые были указаны в массиве PROPS_LIST
// метка о том, что это первый вызов после конструктора
if (isInitial) {
// добавляем поле "name" с нужным значением "value" к возвращаемому изменению состояния
add('name', value);
// добавляем поле "name" со значением взятым из пришедших пропсов
add('name');
}
// changedProps это массив, который содержит имена всех поменявшихся пропсов
if (changedProps.indexOf('value') !== -1) {
add('value');
}
// возвращает true если данный prop как-либо изменился
if (isChanged('autoSize')) {
add('autoSize');
}
// возвращает true если данный prop изменился на указанное значение (здесь на true)
if (isChanged('autoSize', true)) {
add('autoSize', true);
}
// changed является true, если один из пропсов как-либо изменился
if (changed) {
add('somethingChanged', true);
}
// возвращает true, если один из пропсов как-либо изменился
// работает так же, как и пример выше
if (isChangedAny()) {
add('somethingChanged', true);
}
// возвращает true, если один из указанных пропсов как-либо изменился
if (isChangedAny('bgColor', 'fontSize', ...)) {
const {bgColor, fontSize} = nextProps;
add('style', {bgColor, fontSize});
}
// возвращает true, если все пропсы из списка PROPS_LIST как-либо изменились
if (isChangedAll()) {
add('allChanged', true);
}
// возвращает true, если все из указанных пропсов как-либо изменились
if (isChangedAll('width', 'height', ...)) {
const {width, height} = nextProps;
add('size', width + 'x' + height);
// вызывает функцию с таймаутом
// то же самое, что и setTimeout(() => this.changeSomething(), 0);
// используйте для каких-либо действий, которые нужно выполнить по завершению апдейта компонента
// хотя правильнее располагать этот вызов в componentDidUpdate
call(() => {
this.initNewSizes(width, height);
});
}
// вызывает метод "add", если указанный prop как-либо изменился
addIfChanged('name', value);
addIfChanged('name');
// вызывает метод "add", если какой-либо prop из списка PROPS_LIST как-либо изменился
addIfChangedAny('name', value);
addIfChangedAny('name');
// возвращает объект изменения состояния или null
// нужно для отладки, чтобы знать, что ушло в состояние
// располагайте в конце
console.log(get());
// если вы использовали метод "add", то возвращать ничего не нужно
// или вы можете просто вернуть объект, как и обычно без всяких вызовов "add"
return {
size: nextProps.width + 'x' + nextProps.height
}
}
constructor(props) {
super(props);
// используйте "registerContext", если вам необходим this контекст в getDerivedStateFromProps
// если компонент наследуется от другого, в котором был вызван "registerContext", то здесь этого делать не нужно
registerContext(this);
}
// данный метод также будет модифицирован
componentDidUpdate(data) {
const {
prevProps,
prevState,
snapshot,
changedProps,
changed,
isChanged,
isChangedAny,
isChangedAll
} = data;
if (isChanged('value')) {
const {value} = this.props;
this.doSomeAction(value);
}
}
componentWillUnmount() {
// также добавляйте этот код, если "registerContext" был вызван в конструкторе
unregisterContext(this);
}
render() {
const {style, size} = this.state;
return (
<div className="container" style={style}>
Size is {size}
</div>
)
}
}
export const Container = withStateMaster(ContainerComponent, PROP_LIST, INITIAL_STATE);
Если компонент наследуется от другого, передайте родителя, чтобы родительский getDerivedStateFromProps был вызван
export const Container = withStateMaster(ContainerComponent, PROP_LIST, null, ParentalComponent);
Такого мое решение данной проблемы (хотя возможно я недостаточно понял реакт, и это вовсе не проблема).
Таким образом я вступил в сопротивление новым канонам Реакта, возможно когда-нибудь я смирюсь и перепишу все как надо.
Хотя разработчики возможно опять все переделают и возникнут другие насущные вопросы.
Всё, я ложусь, а лежачих, как говорится, не бьют.
Комментарии (8)
justboris
29.08.2018 23:56+1Не хватает практического примера, где это может быть полезно.
В статье есть пример кода со здоровенным
getDerivedStateFromProps
(чтобы показать всё многообразие API, вероятно), но из всех свойств в render используется только два, и от prevProps они никак не зависят
SeaBreeze876
Не получается придумать ситуацию, при которой было бы нужно обновлять состояние на основе предыдущих значений props. Есть примерчик?
Появляется шикарная возможность использовать ES7 синтаксисНа мой взгляд withStateMaster был бы удобнее декоратором
bushstas Автор
Ну вот, например, у меня есть компонент Select, я хочу, чтобы он мог принимать вместо опций промис, тогда мне нужно будет хранить опции в состоянии, то есть подождать исполнения промиса и обновить состояние, которое будет содержать новый массив опций, ясно понятно, что реакт разработчики рекомендуют использовать полностью controlled или полностью uncontrolled компоненты, но не знаю, иногда нужно что-то записывать в состояние и не всегда мемоизация решит эту проблему. Спасибо за совет с декоратором, я попытаюсь это реализовать)
SeaBreeze876
Думаю, разрешению промисов место в componentDidMount и componentDidUpdate. Они отлично для этого подходят. Последний принимает prevProps, и в обоих доступен контекст.
Finesse
Пример: нужно изменить значение атрибута в
state
, когда меняется значение определённого атрибута вprops
. Так предлагают делать создатели React:Как решить эту задачу без использования предыдущего значения
props.type
?Можно использовать
setState
внутриcomponentDidUpdate
, но тогда рендер будет происходить 2 раза (после получения новыхprops
и после вызоваsetState
).MadLord
shouldComponentUpdate()?..
Finesse
Какой из 2-х рендеров вы предлагаете отменить? Если первый, то всё равно будет 2 рендера, потому что скорее всего, когда компонент получает новые
props
, происходит рендер его родителя. Покажите, пожалуйста, пример решения моей задачи вашим способом.MadLord
Я пока не использовал getDerivedStateFromProps(), но в componentWillReceiveProps() использую только nextProps. Да, первого рендера не избежать в любом случае — для минимизации его я делаю так:
Далее для избежания ненужных рендеров:
Ни и промисы с обновлением состояния тоже только когда надо:
Возможно что-то не учел (например, изменение this.props.sid, но проверку на это можно сделать также в shouldComponentUpdate()) или не допонял — не пинайте сильно…