Обновление состояния при изменении реквизита в React Form

184

У меня проблемы с формой React и правильным управлением состоянием. У меня есть поле ввода времени в форме (в модальном). Начальное значение задается как переменная состояния в getInitialStateи передается из родительского компонента. Это само по себе прекрасно работает.

Проблема возникает, когда я хочу обновить значение start_time по умолчанию через родительский компонент. Само обновление происходит в родительском компоненте через setState start_time: new_time. Однако в моей форме значение start_time по умолчанию никогда не меняется, поскольку оно определяется только один раз getInitialState.

Я пытался использовать componentWillUpdateдля принудительного изменения состояния через setState start_time: next_props.start_time, который на самом деле работал, но дал мне Uncaught RangeError: Maximum call stack size exceededошибки.

Итак, мой вопрос, каков правильный способ обновления состояния в этом случае? Думаю ли я об этом как-то не так?

Текущий код:

@ModalBody = React.createClass
  getInitialState: ->
    start_time: @props.start_time.format("HH:mm")

  #works but takes long and causes:
  #"Uncaught RangeError: Maximum call stack size exceeded"
  componentWillUpdate: (next_props, next_state) ->
    @setState(start_time: next_props.start_time.format("HH:mm"))

  fieldChanged: (fieldName, event) ->
    stateUpdate = {}
    stateUpdate[fieldName] = event.target.value
    @setState(stateUpdate)

  render: ->
    React.DOM.div
      className: "modal-body"
      React.DOM.form null,
        React.createElement FormLabelInputField,
          type: "time"
          id: "start_time"
          label_name: "Start Time"
          value: @state.start_time
          onChange: @fieldChanged.bind(null, "start_time”)

@FormLabelInputField = React.createClass
  render: ->
    React.DOM.div
      className: "form-group"
      React.DOM.label
        htmlFor: @props.id
        @props.label_name + ": "
      React.DOM.input
        className: "form-control"
        type: @props.type
        id: @props.id
        value: @props.value
        onChange: @props.onChange
Дэвид Басалла
источник

Ответы:

287

componentWillReceiveProps устарел с момента реакции 16: вместо этого используйте getDerivedStateFromProps

Если я правильно понимаю, у вас есть родительский компонент, проходящий start_timeвниз к ModalBodyкомпоненту , который назначает его свое состояние? И вы хотите обновить это время от родительского, а не дочернего компонента.

У React есть несколько советов по работе с этим сценарием. (Обратите внимание, что это старая статья, которая с тех пор была удалена из Интернета. Вот ссылка на текущий документ по компонентам ).

Использование реквизита для генерации состояния getInitialStateчасто приводит к дублированию «источника правды», то есть того, где находятся реальные данные. Это потому, что getInitialStateвызывается только при первом создании компонента.

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

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

componentWillReceiveProps(nextProps) {
  // You don't have to do this check first, but it can help prevent an unneeded render
  if (nextProps.startTime !== this.state.startTime) {
    this.setState({ startTime: nextProps.startTime });
  }
}
Брэд Б
источник
84
Устаревший с Реакта 16
чувак
7
@ Dude Это еще не устарело, то, что вы имеете в виду, это просто намеки на будущее. Я цитирую[..]going to be deprecated in the future
паддотк
7
@poepje Возможно, это еще не устарело, но по нынешним стандартам оно считается небезопасным, и его, вероятно, следует избегать
раскрывает
12
Итак, каким должен быть новый способ сделать это после того, как componentWillReceiveProps устарел?
Борис Дмитриевич Теохаров
5
@Boris Теперь команда реагирования в основном говорит вам, чтобы вас напичкали. Они дают вам новый метод, называемый getDerivedStateFromProps. Подвох в том, что это статический метод. Это означает, что вы не можете делать ничего асинхронного для обновления состояния (потому что вы должны немедленно вернуть новое состояние), также вы не можете получить доступ к методам или полям класса. Вы также можете использовать памятку, но это не подходит для каждого варианта использования. Еще раз, команда реагирования хочет сбить их с толку. Это крайне глупое и неуместное дизайнерское решение.
ig-dev
76

Видимо, все меняется .... getDerivedStateFromProps () теперь является предпочтительной функцией.

class Component extends React.Component {
  static getDerivedStateFromProps(props, current_state) {
    if (current_state.value !== props.value) {
      return {
        value: props.value,
        computed_prop: heavy_computation(props.value)
      }
    }
    return null
  }
}

(приведенный выше код от danburzo @ github)

ErichBSchulz
источник
7
К вашему сведению, вы должны вернуться также, nullесли ничего не изменится, сразу после вашего if, вы должны пойти сreturn null
Ilgıt Yıldırım
@ IlgıtYıldırım - отредактировал код, так как 4 человека проголосовали за ваш комментарий - действительно ли это имеет значение?
ErichBSchulz
Существует довольно хороший ресурс, в котором подробно рассматриваются различные варианты и причины, по которым вы должны использовать любой из них, getDerivedStateFromPropsили напоминание о
реактиве
2
getDerivedStateFromProps должен быть статическим. Это означает, что вы не можете делать ничего асинхронного для обновления состояния, также вы не можете получить доступ к методам или полям класса. Еще раз, команда реагирования хочет сбить их с толку. Это крайне глупое и неуместное дизайнерское решение.
ig-dev
39

componentWillReceiveProps не рекомендуется, потому что его использование "часто приводит к ошибкам и несоответствиям".

Если что-то меняется извне, рассмотрите возможность полного сброса дочернего компонента с помощьюkey .

Предоставление keyреквизита для дочернего компонента гарантирует, что всякий раз, когда значение keyизменяется извне, этот компонент перерисовывается. Например,

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

По его производительности:

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

Lucia
источник
1
Ключ, секрет! Прекрасно работает в React 16, как упомянуто выше
Даррен Суини
клавиша не будет работать, если это объект, а у вас нет уникальной строки
user3468806
Ключ работает для объектов, я сделал это. Конечно, у меня была уникальная строка для ключа.
tsujp
@ user3468806 Если это не сложный объект с внешними ссылками, вы можете использовать его JSON.stringify(myObject)для получения уникального ключа из вашего объекта.
Рой Принс
24

Также есть компонент componentDidUpdate .

Функция подписи:

componentDidUpdate(prevProps, prevState, snapshot)

Используйте это как возможность работать на DOM, когда компонент был обновлен. Не вызывается на начальном этапе render.

Увидимся, вам, вероятно, не нужна производная статья состояния, которая описывает Anti-Pattern для обоих componentDidUpdateи getDerivedStateFromProps. Я нахожу это очень полезным.

arminfro
источник
Я заканчиваю тем, что использую, componentDidUpdateпотому что это просто и больше подходит для большинства случаев.
KeitelDOG
14

Новый способ перехвата состоит в том, чтобы использовать useEffect вместо componentWillReceiveProps старым способом:

componentWillReceiveProps(nextProps) {
  // You don't have to do this check first, but it can help prevent an unneeded render
  if (nextProps.startTime !== this.state.startTime) {
    this.setState({ startTime: nextProps.startTime });
  }
}

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

// store the startTime prop in local state
const [startTime, setStartTime] = useState(props.startTime)
// 
useEffect(() => {
  if (props.startTime !== startTime) {
    setStartTime(props.startTime);
  }
}, [props.startTime]);

мы устанавливаем состояние с помощью setState, используя useEffect, мы проверяем изменения указанного объекта и предпринимаем действия для обновления состояния при изменении объекта.

MMO
источник
5

Вам, вероятно, не нужно производное состояние

1. Установить ключ от родителя

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

2. Используйте getDerivedStateFromProps/componentWillReceiveProps

Если по какой-то причине ключ не работает (возможно, компонент очень дорог для инициализации)

При использовании getDerivedStateFromPropsвы можете сбросить любую часть состояния, но в настоящее время она выглядит немного глючной (v16.7)!, См. Ссылку выше для использования.

Ghominejad
источник
2

Из реагирующей документации: https://reactjs.org/blog/2018/06/07/you-probbly-dont-need-derived-state.html

Стирание состояния при смене реквизита - это Anti Pattern

Начиная с версии 16, componentWillReceiveProps устарела. Из реактивной документации, рекомендуемый подход в этом случае является использование

  1. Полностью контролируемый компонент: ParentComponentиз ModalBodyбудет принадлежать start_timeгосударству. Это не мой предпочтительный подход в этом случае, так как я думаю, что модал должен владеть этим состоянием.
  2. Полностью неконтролируемый компонент с ключом: это мой предпочтительный подход. Пример из реагирующей документации: https://codesandbox.io/s/6v1znlxyxn . Вы бы полностью владели start_timeгосударством ModalBodyи использовали его так getInitialStateже, как вы уже сделали. Чтобы сбросить start_timeсостояние, вы просто измените ключ сParentComponent
Лу Тран
источник
0

Используйте Memoize

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

Учитывая, что значение состояния start_timeявляется просто опорой start_time.format("HH:mm"), информация, содержащаяся в опоре, уже сама по себе достаточна для обновления компонента.

Однако, если вы хотите вызывать формат только при смене реквизита, правильный способ сделать это для последней документации - через Memoize: https://reactjs.org/blog/2018/06/07/you-probbly-dont- потребность-наследованный state.html # каких-о-запоминании

DannyMoshe
источник
-1

Я думаю, что использование ref безопасно для меня, не нужно заботиться о каком-либо методе выше.

class Company extends XComponent {
    constructor(props) {
        super(props);
        this.data = {};
    }
    fetchData(data) {
        this.resetState(data);
    }
    render() {
        return (
            <Input ref={c => this.data['name'] = c} type="text" className="form-control" />
        );
    }
}
class XComponent extends Component {
    resetState(obj) {
        for (var property in obj) {
            if (obj.hasOwnProperty(property) && typeof this.data[property] !== 'undefined') {
                if ( obj[property] !== this.data[property].state.value )
                    this.data[property].setState({value: obj[property]});
                else continue;
            }
            continue;
        }
    }
}
Майская Погода VN
источник
Я думаю, что этот ответ является загадочным (код трудно читаемый и без какого-либо объяснения / ссылки на проблему OP) и не решает проблему OP, которая заключается в том, как обрабатывать начальное состояние.
нетчкин