React - setState () na niezmontowanym komponencie

92

W moim komponencie reagowania próbuję zaimplementować prosty spinner, gdy trwa żądanie Ajax - używam stanu do przechowywania statusu ładowania.

Z jakiegoś powodu ten fragment kodu poniżej w moim komponencie React zgłasza ten błąd

Może aktualizować tylko zamontowany lub montowany komponent. Zwykle oznacza to, że wywołałeś setState () na niezmontowanym komponencie. To nie jest operacja. Sprawdź kod niezdefiniowanego komponentu.

Jeśli pozbędę się pierwszego wywołania setState, błąd zniknie.

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>
      );
  }

Pytanie brzmi, dlaczego otrzymuję ten błąd, gdy komponent powinien być już zamontowany (jak jest wywoływany z componentDidMount). Myślałem, że można bezpiecznie ustawić stan po zamontowaniu komponentu?

Marty
źródło
w moim konstruktorze ustawiam "this.loadSearches = this.loadSearches.bind (this);" - dodam to do pytania
Marty
czy próbowałeś ustawić ładowanie na null w konstruktorze? To może zadziałać. this.state = { loading : null };
Pramesh Bajracharya

Odpowiedzi:

69

Bez zobaczenia funkcji renderowania jest trochę trudne. Chociaż możesz już dostrzec coś, co powinieneś zrobić, za każdym razem, gdy używasz interwału, musisz go wyczyścić przy odmontowaniu. Więc:

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

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

Ponieważ te wywołania zwrotne sukcesu i błędu mogą nadal być wywoływane po odmontowaniu, możesz użyć zmiennej interwału, aby sprawdzić, czy jest zamontowana.

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

Mam nadzieję, że to pomoże, zapewnij funkcję renderowania, jeśli to nie zadziała.

Twoje zdrowie

Bruno Mota
źródło
2
Bruno, czy nie mógłbyś po prostu przetestować istnienia „tego” kontekstu .. ala this && this.setState .....
james emanon
7
Albo po prostu:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz 10.10.17
@GregHerbowicz jeśli odmontowujesz i montujesz komponent z timerem, nadal można go odpalić, nawet jeśli wykonasz proste czyszczenie.
corlaez,
14

Pytanie brzmi, dlaczego otrzymuję ten błąd, gdy komponent powinien być już zamontowany (jak jest wywoływany z componentDidMount). Myślałem, że można bezpiecznie ustawić stan po zamontowaniu komponentu?

To jest nie wywoływana z componentDidMount. Twoje componentDidMountikra funkcji zwrotnej, która zostanie wykonana w stosie obsługi timera, a nie w stosie componentDidMount. Najwyraźniej do czasu wykonania funkcji callback ( this.loadSearches) komponent został odmontowany.

Tak więc przyjęta odpowiedź cię ochroni. Jeśli używasz innego asynchronicznego interfejsu API, który nie pozwala na anulowanie funkcji asynchronicznych (już przesłanych do jakiejś obsługi), możesz wykonać następujące czynności:

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

Pozwoli to usunąć komunikat o błędzie, który zgłaszasz we wszystkich przypadkach, chociaż wydaje się, że zamiatanie rzeczy pod dywanikiem, szczególnie jeśli twoje API zapewnia możliwość anulowania (tak setIntervaljak w przypadku clearInterval).

Marcus Junius Brutus
źródło
13
isMountedjest antywzorzec projektowy, że Facebook radzi nie używać: facebook.github.io/react/blog/2015/12/16/...
Marty
1
Tak, mówię, że „to naprawdę przypomina zamiatanie rzeczy pod dywan”.
Marcus Junius Brutus
5

Kto potrzebuje innej opcji, metoda wywołania zwrotnego atrybutu ref może być obejściem. Parametr handleRef jest odniesieniem do elementu div DOM.

Szczegółowe informacje na temat referencji i 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>
 )
}
burakhan alkan
źródło
5
Skuteczne użycie ref dla „isMounted” jest dokładnie tym samym, co zwykłe użycie isMounted, ale jest mniej jasne. isMounted nie jest anty-wzorcem ze względu na swoją nazwę, ale ponieważ jest anty-wzorcem przechowującym odniesienia do niezamontowanego komponentu.
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
źródło
Czy istnieje sposób na osiągnięcie tego w przypadku komponentu funkcjonalnego? @john_per
Tamjid
W przypadku komponentu funkcji użyłbym ref: const _isMounted = useRef (false); @Tamjid
john_per
1

Dla potomnych,

Ten błąd w naszym przypadku był związany z refluksem, callbackami, przekierowaniami i setState. Wysłaliśmy setState do wywołania zwrotnego onDone, ale wysłaliśmy również przekierowanie do wywołania zwrotnego onSuccess. W przypadku powodzenia nasze wywołanie zwrotne onSuccess jest wykonywane przed onDone . To powoduje przekierowanie przed próbą setstate . Tak więc błąd, setState na niezmontowanym komponencie.

Akcja sklepu Reflux:

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

Zadzwoń przed naprawą:

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

Zadzwoń po naprawie:

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

Jeszcze

W niektórych przypadkach, ponieważ isMounted Reacta jest "przestarzałe / anty-wzorzec", przyjęliśmy użycie zmiennej _mounted i sami ją monitorowaliśmy.

Geoffrey Hale
źródło
1

Udostępnij rozwiązanie, które umożliwia reagowanie hookami .

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)
}, [])

to samo rozwiązanie można rozszerzyć na dowolne miejsce, w którym chcesz anulować poprzednie żądania przy zmianie identyfikatora pobierania, w przeciwnym razie wystąpią warunki wyścigu między wieloma żądaniami w locie ( this.setStatewywołanymi poza kolejnością).

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

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

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

działa to dzięki domknięciom w javascript.

Ogólnie rzecz biorąc, powyższy pomysł był zbliżony do podejścia makeCancelable zalecanego w dokumencie react doc, które jasno stwierdza

isMounted to Antipattern

Kredyt

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

Xlee
źródło