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.

Marty
fuente
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

Bruno Mota
fuente
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).

Marco Junio ​​Bruto
fuente
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>
 )
}
burakhan alkan
fuente
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() {
    ...
  }
}
john_per
fuente
¿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.

Geoffrey Hale
fuente
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/

Xlee
fuente