React - setState () на размонтированном компоненте

92

В моем компоненте реакции я пытаюсь реализовать простой счетчик, пока выполняется запрос ajax - я использую состояние для хранения статуса загрузки.

По какой-то причине этот фрагмент кода ниже в моем компоненте React выдает эту ошибку

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

Если я избавлюсь от первого вызова setState, ошибка исчезнет.

constructor(props) {
  super(props);
  this.loadSearches = this.loadSearches.bind(this);

  this.state = {
    loading: false
  }
}

loadSearches() {

  this.setState({
    loading: true,
    searches: []
  });

  console.log('Loading Searches..');

  $.ajax({
    url: this.props.source + '?projectId=' + this.props.projectId,
    dataType: 'json',
    crossDomain: true,
    success: function(data) {
      this.setState({
        loading: false
      });
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
      this.setState({
        loading: false
      });
    }.bind(this)
  });
}

componentDidMount() {
  setInterval(this.loadSearches, this.props.pollInterval);
}

render() {

    let searches = this.state.searches || [];


    return (<div>
          <Table striped bordered condensed hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Submit Date</th>
              <th>Dataset &amp; Datatype</th>
              <th>Results</th>
              <th>Last Downloaded</th>
            </tr>
          </thead>
          {
          searches.map(function(search) {

                let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
                let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
                let records = 0;
                let status = search.status ? search.status.toLowerCase() : ''

                return (
                <tbody key={search.id}>
                  <tr>
                    <td>{search.name}</td>
                    <td>{createdDate}</td>
                    <td>{search.dataset}</td>
                    <td>{records}</td>
                    <td>{downloadedDate}</td>
                  </tr>
                </tbody>
              );
          }
          </Table >
          </div>
      );
  }

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

Марти
источник
в моем конструкторе я устанавливаю this.loadSearches = this.loadSearches.bind (this); - Я добавлю это к вопросу
Марти
вы пробовали установить для загрузки значение null в своем конструкторе? Это может сработать. this.state = { loading : null };
Pramesh Bajracharya

Ответы:

69

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

componentDidMount() {
    this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}

componentWillUnmount () {
    this.loadInterval && clearInterval(this.loadInterval);
    this.loadInterval = false;
}

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

this.loadInterval && this.setState({
    loading: false
});

Надеюсь, это поможет, предоставьте функцию рендеринга, если это не работает.

Ура

Бруно Мота
источник
2
Бруно, не могли бы вы просто проверить наличие «этого» контекста .. аля this && this.setState .....
Джеймс Эманон
7
Или просто:componentWillUnmount() { clearInterval(this.loadInterval); }
Грег Хербович
@GregHerbowicz, если вы размонтируете и монтируете компонент с таймером, он все равно может быть запущен, даже если вы выполните простую очистку.
Corlaez
14

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

Это не вызывается из componentDidMount. Вы componentDidMountсоздаете функцию обратного вызова, которая будет выполняться в стеке обработчика таймера, а не в стеке componentDidMount. Очевидно, к моменту выполнения вашего callback ( this.loadSearches) компонент уже отключился.

Так что принятый ответ защитит вас. Если вы используете какой-либо другой асинхронный API, который не позволяет вам отменять асинхронные функции (уже отправленные какому-либо обработчику), вы можете сделать следующее:

if (this.isMounted())
     this.setState(...

Это избавит вас от сообщения об ошибке, о котором вы сообщаете во всех случаях, хотя это действительно похоже на подметание вещей, особенно если ваш API предоставляет возможность отмены (как и в setIntervalслучае с clearInterval).

Марк Юний Брут
источник
13
isMounted- это антипаттерн, который facebook советует не использовать: facebook.github.io/react/blog/2015/12/16/…
Марти,
1
Да, я действительно говорю, что «это похоже на подметание вещей под ковер».
Marcus Junius Brutus
5

Для кого нужен другой вариант, метод обратного вызова атрибута ref может быть обходным решением. Параметр handleRef - это ссылка на элемент DOM div.

Для получения подробной информации о refs и DOM: https://facebook.github.io/react/docs/refs-and-the-dom.html

handleRef = (divElement) => {
 if(divElement){
  //set state here
 }
}

render(){
 return (
  <div ref={this.handleRef}>
  </div>
 )
}
бурахан алкан
источник
5
Использование ссылки для эффективного "isMounted" - это то же самое, что просто использование isMounted, но менее понятное. isMounted не является анти-шаблоном из-за своего имени, а потому, что он является анти-шаблоном для хранения ссылок на размонтированный компонент.
Pajn
3
class myClass extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      data: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;
    this._getData();
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _getData() {
    axios.get('https://example.com')
      .then(data => {
        if (this._isMounted) {
          this.setState({ data })
        }
      });
  }


  render() {
    ...
  }
}
john_per
источник
Есть ли способ добиться этого для функционального компонента? @john_per
Тамджид
Для функционального компонента я бы использовал ref: const _isMounted = useRef (false); @Tamjid
john_per
1

Для потомков,

Эта ошибка в нашем случае была связана с Reflux, обратными вызовами, перенаправлениями и setState. Мы отправили setState в обратный вызов onDone, но мы также отправили перенаправление в обратный вызов onSuccess. В случае успеха наш обратный вызов onSuccess выполняется до onDone . Это вызывает перенаправление до попытки setState . Таким образом, ошибка setState на отключенном компоненте.

Действие магазина рефлюкса:

generateWorkflow: function(
    workflowTemplate,
    trackingNumber,
    done,
    onSuccess,
    onFail)
{...

Позвоните перед исправлением:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    this.setLoading.bind(this, false),
    this.successRedirect
);

Звоните после исправления:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    null,
    this.successRedirect,
    this.setLoading.bind(this, false)
);

Больше

В некоторых случаях, поскольку isMounted в React является «устаревшим / анти-паттерном», мы приняли использование переменной _mounted и сами отслеживаем ее.

Джеффри Хейл
источник
1

Поделитесь решением с помощью обработчиков реакции .

React.useEffect(() => {
  let isSubscribed = true

  callApi(...)
    .catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
    .then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
    .catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))

  return () => (isSubscribed = false)
}, [])

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

React.useEffect(() => {
  let isCancelled = false

  callApi(id).then(...).catch(...) // similar to above

  return () => (isCancelled = true)
}, [id])

это работает благодаря закрытию в javascript.

В целом, идея выше была близка к подходу makeCancelable, рекомендованному в React doc, в котором четко указано

isMounted - это антипаттерн

Кредит

https://juliangaramendy.dev/use-promise-subscription/

Xlee
источник