Reaccionar - setState () en el componente desmontado


92

En mi componente de reacción, estoy tratando de implementar un spinner simple mientras una solicitud ajax está en progreso; estoy usando el estado para almacenar el estado de carga.

Por alguna razón, este fragmento de código a continuación en mi componente React arroja este error

Solo se puede actualizar un componente montado o de montaje. Por lo general, esto significa que llamó a setState () en un componente desmontado. Esta es una operación no operativa. Compruebe el código del componente indefinido.

Si me deshago de la primera llamada setState, el error desaparece.

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

La pregunta es por qué recibo este error cuando el componente ya debería estar montado (ya que se llama desde componentDidMount). Pensé que era seguro establecer el estado una vez que el componente está montado.


en mi constructor estoy configurando "this.loadSearches = this.loadSearches.bind (this);" - Agregaré eso a la pregunta
Marty

¿Ha intentado configurar la carga en nulo en su constructor? Aquello podría funcionar. this.state = { loading : null };
Pramesh Bajracharya

Respuestas:


69

Sin ver la función de renderizado es un poco complicado. Aunque ya puede detectar algo que debe hacer, cada vez que usa un intervalo, debe borrarlo al desmontar. Entonces:

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

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

Dado que esas devoluciones de llamada de éxito y error aún se pueden llamar después del desmontaje, puede usar la variable de intervalo para verificar si está montada.

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

Espero que esto ayude, proporcione la función de renderizado si esto no funciona.

Salud


2
Bruno, ¿no podrías simplemente probar la existencia de "este" contexto ... ala this && this.setState .....
james emanon

6
O simplemente:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowicz, si está desmontando y montando el componente con el temporizador, aún puede dispararse incluso si realiza la limpieza simple.
corlaez

14

La pregunta es por qué recibo este error cuando el componente ya debería estar montado (ya que se llama desde componentDidMount). Pensé que era seguro establecer el estado una vez que el componente está montado.

Se no llamó desde componentDidMount. Su componentDidMountgenera una función de devolución de llamada que se ejecutará en la pila del controlador del temporizador, no en la pila de componentDidMount. Aparentemente, para cuando this.loadSearchesse ejecuta su callback ( ), el componente se ha desmontado.

Entonces la respuesta aceptada lo protegerá. Si está utilizando alguna otra API asincrónica que no le permite cancelar funciones asincrónicas (ya enviadas a algún controlador), puede hacer lo siguiente:

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

Esto eliminará el mensaje de error que informa en todos los casos, aunque se siente como si estuviera escondiendo cosas debajo de la alfombra, particularmente si su API proporciona una capacidad de cancelación (como lo setIntervalhace con clearInterval).


12
isMountedes un antipatrón que Facebook aconseja no utilizar: facebook.github.io/react/blog/2015/12/16/…
Marty

1
Sí, digo que "se siente como barrer cosas debajo de la alfombra".
Marcus Junius Brutus

5

Para quién necesita otra opción, el método de devolución de llamada del atributo ref puede ser una solución. El parámetro de handleRef es la referencia al elemento div DOM.

Para obtener información detallada sobre referencias y 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
Usar una referencia para efectivamente "isMounted" es exactamente lo mismo que usar isMounted pero menos claro. isMounted no es un anti-patrón debido a su nombre, sino porque es un anti-patrón para contener referencias a un componente desmontado.
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() {
    ...
  }
}

¿Hay alguna forma de lograr esto para un componente funcional? @john_per
Tamjid

Para un componente de función, usaría ref: const _isMounted = useRef (false); @Tamjid
john_per

1

Para la posteridad,

Este error, en nuestro caso, estaba relacionado con Reflux, callbacks, redirects y setState. Enviamos un setState a una devolución de llamada onDone, pero también enviamos una redirección a la devolución de llamada onSuccess. En caso de éxito, nuestra devolución de llamada onSuccess se ejecuta antes que onDone . Esto provoca una redirección antes del intento setState . Por lo tanto, el error setState en un componente desmontado.

Acción de almacenamiento de reflujo:

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

Llame antes de arreglar:

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

Llamar después de arreglar:

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

Más

En algunos casos, dado que React's isMounted está "obsoleto / anti-patrón", hemos adoptado el uso de una variable _mounted y la monitoreamos nosotros mismos.


1

Comparta una solución habilitada por react hooks .

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

la misma solución puede extenderse siempre que desee cancelar solicitudes anteriores sobre cambios de identificación de búsqueda; de lo contrario, habría condiciones de carrera entre múltiples solicitudes en vuelo ( this.setStatellamadas fuera de servicio).

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

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

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

esto funciona gracias a cierres en javascript.

En general, la idea anterior estaba cerca del enfoque makeCancelable recomendado por react doc, que establece claramente

isMounted es un antipatrón

Crédito

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.