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 };
Прамеш Байрачар'я,

Відповіді:


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.setState .....
Джеймс Еманон

7
Або просто:componentWillUnmount() { clearInterval(this.loadInterval); }
Грег Гербовіч

@GregHerbowicz, якщо ви демонтуєте та монтуєте компонент за допомогою таймера, він все одно може спрацювати, навіть якщо ви виконаєте просте очищення.
corlaez

14

Питання в тому, чому я отримую цю помилку, коли компонент вже повинен бути змонтований (як його викликають з componentDidMount), я вважав безпечним встановлення стану після монтажу компонента?

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

Тож прийнята відповідь захистить вас. Якщо ви використовуєте інший асинхронний API, який не дозволяє скасувати асинхронні функції (вже надіслані якомусь обробнику), ви можете зробити наступне:

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

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


13
isMountedє антипаттерн , що facebook радить не використовувати: facebook.github.io/react/blog/2015/12/16 / ...
Marty

1
Так, я кажу, що "це відчуває себе як підмітання речей під килимом".
Маркус Юній Брут,

5

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

Для отримання детальної інформації про посилання та 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
Використання ref для ефективного "isMount" - це точно те саме, що просто використання isMount, але менш зрозуміле. isMount є не анти-шаблоном через свою назву, а тому, що він є анти-шаблоном для зберігання посилань на немонтований компонент.
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
Tamjid

Для функціонального компонента я б використав ref: const _isMount = 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)
);

Більше

У деяких випадках, оскільки React's isMount є "застарілим / анти-шаблоном", ми прийняли використання змінної _mount і контролюємо це самостійно.


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, рекомендованого документом реакції, в якому чітко зазначено

isMount - це антишаблон

Кредит

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.