Решение отсутствия prevProps в getDerivedStateFromProps

в 8:59, , рубрики: componentDidUpdate, componentWillReceiveProps, getDerivedStateFromProps, javascript, React, ReactJS

Привет, друзья!

Итак, разработчики Реакта решили сделать нашу работу с их либой более линейной, направить, так сказать, нас нерадивых на путь наименьшего шанса ошибиться и написать плохой код, что, на мой взгляд, является нашим неотъемлемым правом и способом совершенствоваться и изобретать. Речь идет о всеми любимых методах 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);

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

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

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

Автор: bushstas

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js