В чем преимущество использования componentDidUpdate по сравнению с обратным вызовом setState? - PullRequest
5 голосов
/ 08 июня 2019

Почему рекомендуется использовать componentDidUpdate вместо функции обратного вызова setState (необязательный второй аргумент) в компонентах React (если требуется синхронное поведение setState)?

Поскольку setState является асинхронным, я думал об использовании функции обратного вызова setState (2-й аргумент), чтобы гарантировать выполнение кода после обновления состояния, аналогично then() для обещаний.Особенно, если мне нужен повторный рендеринг между последующими setState вызовами.

Однако официальные документы React говорят: «Второй параметр для setState () - это дополнительная функция обратного вызова, которая будет выполнена после завершения setState.и компонент повторно визуализируется. Обычно мы рекомендуем использовать componentDidUpdate () для такой логики. "И это все, что они говорят об этом там, так что это кажется немного расплывчатым.Мне было интересно, если есть более конкретная причина, рекомендуется не использовать его?Если бы я мог, я бы спросил самих людей React.

Если я хочу, чтобы несколько вызовов setState выполнялись последовательно, обратный вызов setState кажется лучшим выбором по сравнению с componentDidUpdate с точки зрения организации кода - код обратного вызова определен правильнотам с вызовом setState.Если я использую componentDidUpdate, я должен проверить, изменилась ли соответствующая переменная состояния, и определить там следующий код, который будет труднее отслеживать.Кроме того, переменные, которые были определены в функции, содержащей вызов setState, выходили бы из области видимости, если бы я тоже не помещал их в состояние.

Следующий пример может показать, когда может быть сложно использовать componentDidUpdate:

private functionInComponent = () => {
  let someVariableBeforeSetStateCall; 

  ... // operations done on someVariableBeforeSetStateCall, etc.

  this.setState(
    { firstVariable: firstValue, }, //firstVariable may or may not have been changed
    () => {
       let secondVariable = this.props.functionFromParentComponent();
       secondVariable += someVariableBeforeSetStateCall;
       this.setState({ secondVariable: secondValue });
    }
  );
}

против

public componentDidUpdate(prevProps. prevState) {
   if (prevState.firstVariableWasSet !== this.state.firstVariableWasSet) {
      let secondVariable = this.props.functionFromParentComponent();
      secondVariable += this.state.someVariableBeforeSetStateCall;
      this.setState({ 
        secondVariable: secondValue, 
        firstVariableWasSet: false,
      });
   }
}

private functionInComponent = () => {
  let someVariableBeforeSetStateCall = this.state.someVariableBeforeSetStateCall; 

  ... // operations done on someVariableBeforeSetStateCall, etc.

  this.setState({ 
      firstVariable: firstValue, 
      someVariableBeforeSetStateCall: someVariableBeforeSetStateCall, 
      firstVariableWasSet: true });
  //firstVariable may or may not have been changed via input, 
  //now someVariableBeforeSetStateCall may or may not get updated at the same time 
  //as firstVariableWasSet or firstVariable due to async nature of setState
}

Кроме того, помимо того, что в целом рекомендуется componentDidUpdate, в каких случаях будет более целесообразным использовать обратный вызов setState?

1 Ответ

4 голосов
/ 08 июня 2019

Почему рекомендуется использовать componentDidUpdate больше, чем функция обратного вызова setState?

1. Последовательная логика

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

Типичным примером является вызов сторонней службы при каждом изменении состояния:

private method1(value) {
    this.setState({ value }, () => {
        SomeAPI.gotNewValue(this.state.value);
    });
}

private method2(newval) {
    this.setState({ value }); // forgot callback?
}

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

Вот почему componentDidUpdate() рекомендуется:

public componentDidUpdate(prevProps, prevState) {
    if (this.state.value !== prevState.value) {
        SomeAPI.gotNewValue(this.state.value);
    }
}

private method1(value) {
    this.setState({ value });
}

private method2(newval) {
    this.setState({ value });
}

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

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

2. Пакетные обновления

Аргумент обратного вызова setState() выполняется после повторной визуализации компонента. Однако из-за пакетной обработки несколько вызовов на setState() не гарантированно вызовут несколько обработчиков.

Рассмотрим этот компонент:

class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: 0 };
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate: ' + this.state.value);
  }

  onClick = () => {
    this.setState(
      { value: 7 },
      () => console.log('onClick: ' + this.state.value));
    this.setState(
      { value: 42 },
      () => console.log('onClick: ' + this.state.value));
  }

  render() {
    return <button onClick={this.onClick}>{this.state.value}</button>;
  }
}

У нас есть два setState() вызова в обработчике onClick(), каждый из которых просто выводит новое значение состояния на консоль.

Можно ожидать, что onClick() напечатает значение 7, а затем 42. Но на самом деле он печатает 42 дважды! Это связано с тем, что два вызова setState() объединены и вызывают только один рендеринг.

Также у нас есть componentDidUpdate(), который также печатает новое значение. Поскольку у нас только один рендеринг, он выполняется только один раз и печатает значение 42.

Если вам нужна согласованность с пакетными обновлениями, обычно проще использовать componentDidMount().

2,1. Когда происходит дозирование?

Это не имеет значения.

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

Но, если вам необходимо знать, в текущей версии React (16.8.x) пакетирование происходит в асинхронных пользовательских обработчиках событий (например, onclick) и иногда методах жизненного цикла, если React имеет полный контроль над исполнением. Во всех других контекстах никогда не используется пакетная обработка.

См. Этот ответ для получения дополнительной информации: https://stackoverflow.com/a/48610973/640397

3. Когда лучше использовать обратный вызов setState?

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

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

interface IProps {
    onClick: () => Promise<void>;
}

class Child extends React.Component<IProps> {

    private async click() {
        await this.props.onClick();

        console.log('Parent notified of click');
    }

    render() {
        return <button onClick={this.click}>click me</button>;
    }
}

И у нас есть Parent компонент, который должен обновлять состояние при нажатии дочернего элемента:

class Parent extends React.Component {
    constructor(props) {
        super(props);

        this.state = { clicked: false };
    }

    private setClicked = (): Promise<void> => {
        return new Promise((resolve) => this.setState({ clicked: true }, resolve));
    }

    render() {
        return <Child onClick={this.setClicked} />;
    }
}

В setClicked мы должны создать Promise, чтобы вернуться к потомку, и единственный способ сделать это - передать обратный вызов setState.

Невозможно создать это Promise в componentDidUpdate, но даже если бы оно было, оно не будет работать должным образом из-за пакетирования.

Разное.

Поскольку setState является асинхронным, я думал об использовании функции обратного вызова setState (2-й аргумент), чтобы гарантировать выполнение кода после обновления состояния, аналогично .then() для обещаний.

Обратный вызов для setState() не вполне работает так же, как обещания, поэтому может быть лучше разделить ваши знания.

Особенно, если мне нужен повторный рендеринг между последующими setState вызовами.

Зачем вам когда-либо нужно повторно визуализировать компонент между setState() вызовами?

Единственная причина, которую я могу себе представить, состоит в том, что родительский компонент зависит от некоторой информации из дочернего элемента DOM, такой как его ширина или высота, и родительский элемент устанавливает некоторые реквизиты для дочернего элемента на основе этих значений.

В вашем примере вы вызываете this.props.functionFromParentComponent(), который возвращает значение, которое вы затем используете для вычисления некоторого состояния.

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

  //firstVariable may or may not have been changed, 
  //now someVariableBeforeSetStateCall may or may not get updated at the same time 
  //as firstVariableWasSet or firstVariable due to async nature of setState

Эти комментарии не имеют большого смысла для меня. Асинхронная природа setState() не подразумевает ничего о том, что состояние не обновляется должным образом. Код должен работать как задумано.

...