React - setState () no componente não montado

92

No meu componente de reação, estou tentando implementar um spinner simples enquanto uma solicitação ajax está em andamento - estou usando o estado para armazenar o status de carregamento.

Por alguma razão, este trecho de código abaixo em meu componente React gera este erro

Só pode atualizar um componente montado ou de montagem. Isso geralmente significa que você chamou setState () em um componente não montado. Este é um ambiente autônomo. Verifique o código do componente indefinido.

Se eu me livrar da primeira chamada setState, o erro desaparecerá.

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

A questão é por que estou recebendo este erro quando o componente já deveria estar montado (como está sendo chamado de componentDidMount). Eu pensei que era seguro definir o estado depois que o componente for montado?

Marty
fonte
no meu construtor estou definindo "this.loadSearches = this.loadSearches.bind (this);" - adicionarei isso à pergunta
Marty,
você já tentou definir o carregamento como nulo em seu construtor? Isso pode funcionar. this.state = { loading : null };
Pramesh Bajracharya de

Respostas:

69

Sem ver a função de renderização é um pouco difícil. Embora já possa detectar algo que você deve fazer, toda vez que usar um intervalo, você terá que limpá-lo ao desmontar. Assim:

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

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

Como os retornos de chamada de sucesso e erro ainda podem ser chamados após a desmontagem, você pode usar a variável de intervalo para verificar se está montado.

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

Espero que isso ajude, forneça a função de renderização se isso não funcionar.

Felicidades

Bruno Mota
fonte
2
Bruno, você não poderia apenas testar a existência de "este" contexto .. ala this && this.setState .....
James emanon
6
Ou simplesmente:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz,
@GregHerbowicz se você estiver desmontando e montando o componente com o cronômetro, ele ainda pode ser disparado mesmo se você fizer a limpeza simples.
corlaez
14

A questão é por que estou recebendo este erro quando o componente já deveria estar montado (como está sendo chamado de componentDidMount). Eu pensei que era seguro definir o estado depois que o componente for montado?

É não chamada a partir componentDidMount. Seu componentDidMountgera uma função de retorno de chamada que será executada na pilha do manipulador do cronômetro, não na pilha de componentDidMount. Aparentemente, no momento em que seu callback ( this.loadSearches) é executado, o componente já foi desmontado.

Portanto, a resposta aceita irá protegê-lo. Se você estiver usando alguma outra API assíncrona que não permite o cancelamento de funções assíncronas (já enviadas para algum manipulador), você pode fazer o seguinte:

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

Isso eliminará a mensagem de erro que você relata em todos os casos, embora pareça que está varrendo coisas para debaixo do tapete, particularmente se sua API fornecer um recurso de cancelamento (como setIntervalfaz com clearInterval).

Marcus Junius Brutus
fonte
12
isMountedé um antipadrão que o Facebook recomenda não usar: facebook.github.io/react/blog/2015/12/16/…
Marty
1
Sim, eu digo que "é como jogar coisas para baixo do tapete".
Marcus Junius Brutus
5

Para quem precisa de outra opção, o método de retorno de chamada do atributo ref pode ser uma solução alternativa. O parâmetro de handleRef é a referência ao elemento div DOM.

Para obter informações detalhadas sobre refs e 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 alcan
fonte
5
Usar um ref para efetivamente "isMounted" é exatamente a mesma coisa que usar isMounted, mas menos claro. isMounted não é um antipadrão por causa de seu nome, mas porque é um antipadrão para conter referências a um componente não montado.
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
fonte
Existe uma maneira de fazer isso para um componente funcional? @john_per
Tamjid
Para um componente de função, eu usaria ref: const _isMounted = useRef (false); @Tamjid
john_per
1

Para a posteridade,

Este erro, em nosso caso, estava relacionado ao Reflux, callbacks, redirecionamentos e setState. Enviamos um setState para um retorno de chamada onDone, mas também enviamos um redirecionamento para o retorno de chamada onSuccess. Em caso de sucesso, nosso retorno de chamada onSuccess é executado antes de onDone . Isso causa um redirecionamento antes da tentativa de setState . Portanto, o erro, setState em um componente não montado.

Ação de armazenamento de refluxo:

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

Ligue antes de consertar:

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

Chamada após correção:

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

Mais

Em alguns casos, como o isMounted do React é "obsoleto / anti-padrão", adotamos o uso de uma variável _mounted e monitorando-a nós mesmos.

Geoffrey Hale
fonte
1

Compartilhe uma solução habilitada por ganchos de reação .

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

a mesma solução pode ser estendida para sempre que você quiser cancelar solicitações anteriores nas alterações de id de busca, caso contrário, haveria condições de corrida entre várias solicitações em voo ( this.setStatechamadas fora de serviço).

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

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

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

isso funciona graças a fechamentos em javascript.

Em geral, a ideia acima estava próxima da abordagem makeCancelable recomendada pelo documento de reação, que afirma claramente

isMounted é um antipadrão

Crédito

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

Xlee
fonte