La memoria de limpieza se pierde en un componente desmontado en React Hooks

19

Soy nuevo usando React, por lo que esto podría ser realmente simple de lograr, pero no puedo resolverlo por mí mismo a pesar de que he investigado un poco. Perdóname si esto es demasiado tonto.

Contexto

Estoy usando Inertia.js con los adaptadores Laravel (backend) y React (front-end). Si no conoce la inercia, básicamente:

Inertia.js le permite crear rápidamente aplicaciones modernas React, Vue y Svelte de una sola página utilizando enrutamiento y controladores clásicos del lado del servidor.

Problema

Estoy haciendo una página de inicio de sesión simple que tiene un formulario que, cuando se envíe, realizará una solicitud POST para cargar la página siguiente. Parece funcionar bien, pero en otras páginas la consola muestra la siguiente advertencia:

Advertencia: no se puede realizar una actualización del estado Reaccionar en un componente desmontado. Esto es un no-op, pero indica una pérdida de memoria en su aplicación. Para solucionarlo, cancele todas las suscripciones y tareas asincrónicas en una función de limpieza useEffect.

en inicio de sesión (creado por Inertia)

El código relacionado (lo he simplificado para evitar líneas irrelevantes):

import React, { useEffect, useState } from 'react'
import Layout from "../../Layouts/Auth";

{/** other imports */}

    const login = (props) => {
      const { errors } = usePage();

      const [values, setValues] = useState({email: '', password: '',});
      const [loading, setLoading] = useState(false);

      function handleSubmit(e) {
        e.preventDefault();
        setLoading(true);
        Inertia.post(window.route('login.attempt'), values)
          .then(() => {
              setLoading(false); // Warning : memory leaks during the state update on the unmounted component <--------
           })                                   
      }

      return (
        <Layout title="Access to the system">
          <div>
            <form action={handleSubmit}>
              {/*the login form*/}

              <button type="submit">Access</button>
            </form>
          </div>
        </Layout>
      );
    };

    export default login;

Ahora, sé que tengo que hacer una función de limpieza porque la promesa de la solicitud es lo que está generando esta advertencia. Sé que debería usarlo, useEffectpero no sé cómo aplicarlo en este caso. He visto un ejemplo cuando un valor cambia, pero ¿cómo hacerlo en una llamada de este tipo?

Gracias por adelantado.


Actualizar

Según lo solicitado, el código completo de este componente:

import React, { useState } from 'react'
import Layout from "../../Layouts/Auth";
import { usePage } from '@inertiajs/inertia-react'
import { Inertia } from "@inertiajs/inertia";
import LoadingButton from "../../Shared/LoadingButton";

const login = (props) => {
  const { errors } = usePage();

  const [values, setValues] = useState({email: '', password: '',});

  const [loading, setLoading] = useState(false);

  function handleChange(e) {
    const key = e.target.id;
    const value = e.target.value;

    setValues(values => ({
      ...values,
      [key]: value,
    }))
  }

  function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
      .then(() => {
        setLoading(false);
      })
  }

  return (
    <Layout title="Inicia sesión">
      <div className="w-full flex items-center justify-center">
        <div className="w-full max-w-5xl flex justify-center items-start z-10 font-sans text-sm">
          <div className="w-2/3 text-white mt-6 mr-16">
            <div className="h-16 mb-2 flex items-center">                  
              <span className="uppercase font-bold ml-3 text-lg hidden xl:block">
                Optima spark
              </span>
            </div>
            <h1 className="text-5xl leading-tight pb-4">
              Vuelve inteligente tus operaciones
            </h1>
            <p className="text-lg">
              Recoge data de tus instalaciones de forma automatizada; accede a información histórica y en tiempo real
              para que puedas analizar y tomar mejores decisiones para tu negocio.
            </p>

            <button type="submit" className="bg-yellow-600 w-40 hover:bg-blue-dark text-white font-semibold py-2 px-4 rounded mt-8 shadow-md">
              Más información
            </button>
          </div>

        <div className="w-1/3 flex flex-col">
          <div className="bg-white text-gray-700 shadow-md rounded rounded-lg px-8 pt-6 pb-8 mb-4 flex flex-col">
            <div className="w-full rounded-lg h-16 flex items-center justify-center">
              <span className="uppercase font-bold text-lg">Acceder</span>
            </div>

            <form onSubmit={handleSubmit} className={`relative ${loading ? 'invisible' : 'visible'}`}>

              <div className="mb-4">
                <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="email">
                  Email
                </label>
                <input
                  id="email"
                  type="text"
                  className=" appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
                  placeholder="Introduce tu e-mail.."
                  name="email"
                  value={values.email}
                  onChange={handleChange}
                />
                {errors.email && <p className="text-red-500 text-xs italic">{ errors.email[0] }</p>}
              </div>
              <div className="mb-6">
                <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="password">
                  Contraseña
                </label>
                <input
                  className=" appearance-none border border-red rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
                  id="password"
                  name="password"
                  type="password"
                  placeholder="*********"
                  value={values.password}
                  onChange={handleChange}
                />
                {errors.password && <p className="text-red-500 text-xs italic">{ errors.password[0] }</p>}
              </div>
              <div className="flex flex-col items-start justify-between">
                <LoadingButton loading={loading} label='Iniciar sesión' />

                <a className="font-semibold text-sm text-blue hover:text-blue-700 mt-4"
                   href="#">
                  <u>Olvidé mi contraseña</u>
                </a>
              </div>
              <div
                className={`absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center ${!loading ? 'invisible' : 'visible'}`}
              >
                <div className="lds-ellipsis">
                  <div></div>
                  <div></div>
                  <div></div>
                  <div></div>
                </div>
              </div>
            </form>
          </div>
          <div className="w-full flex justify-center">
            <a href="https://optimaee.com">
            </a>
          </div>
        </div>
        </div>
      </div>
    </Layout>
  );
};

export default login;
Kenny Horna
fuente
@Sohail He agregado el código completo del componente
Kenny Horna
¿Intentaste simplemente eliminar el .then(() => {})?
Guerric P

Respuestas:

22

Debido a que es la llamada de promesa asíncrona, debe usar una variable de referencia mutable (con useRef) para verificar el componente ya desmontado para el próximo tratamiento de la respuesta asíncrona (evitando pérdidas de memoria):

Advertencia: no se puede realizar una actualización del estado Reaccionar en un componente desmontado.

Dos ganchos de reacción que debes usar en este caso: useRefy useEffect.

Con useRef, por ejemplo, la variable mutable _isMountedsiempre apunta a la misma referencia en la memoria (no una variable local)

useRef es el enlace de acceso si se necesita una variable mutable. A diferencia de las variables locales, React se asegura de que se devuelva la misma referencia durante cada render. Si lo desea, es lo mismo con this.myVar en Class Component

Ejemplo:

const login = (props) => {
  const _isMounted = useRef(true); // Initial value _isMounted = true

  useEffect(() => {
    return () => { // ComponentWillUnmount in Class Component
        _isMounted.current = false;
    }
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    ajaxCall = Inertia.post(window.route('login.attempt'), values)
        .then(() => {
            if (_isMounted.current) { // Check always mounted component
               // continue treatment of AJAX response... ;
            }
         )
  }
}

En la misma ocasión, déjame explicarte más información sobre React Hooks que se usa aquí. Además, compararé React Hooks en Functional Component (la versión React> 16.8) con LifeCycle en Class Component.

useEffect : la mayoría de los efectos secundarios ocurren dentro del anzuelo. Ejemplos de efectos secundarios son: recuperación de datos, configuración de una suscripción y cambio manual del DOM en los componentes React. UseEffect reemplaza muchos LifeCycles en Class Component (componentDidMount, componentDidUpate, componentWillUnmount)

 useEffect(fnc, [dependency1, dependency2, ...]); // dependencies array argument is optional

1) El comportamiento predeterminado de useEffect se ejecuta después del primer render (como ComponentDidMount) y después de cada actualización de actualización (como ComponentDidUpdate) si no tiene dependencias. Es así :useEffect(fnc);

2) Dar una variedad de dependencias para usar Effect cambiará su ciclo de vida. En este ejemplo: useEffect se llamará una vez después del primer render y cada vez que el recuento cambie

export default function () {
   const [count, setCount] = useState(0);

   useEffect(fnc, [count]);
}

3) useEffect se ejecutará solo una vez después del primer renderizado (como ComponentDidMount) si coloca una matriz vacía para la dependencia. Es así :useEffect(fnc, []);

4) Para evitar pérdidas de recursos, todo debe desecharse cuando finaliza el ciclo de vida de un gancho (como ComponentWillUnmount) . Por ejemplo, con la matriz vacía de dependencia, la función devuelta se llamará después de que el componente se desmonte. Es así :

useEffect(() => {
   return fnc_cleanUp; // fnc_cleanUp will cancel all subscriptions and asynchronous tasks (ex. : clearInterval) 
}, []);

useRef : devuelve un objeto de referencia mutable cuya propiedad .current se inicializa al argumento pasado (initialValue). El objeto devuelto persistirá durante toda la vida útil del componente.

Ejemplo: con la pregunta anterior, no podemos usar una variable local aquí porque se perderá y se reiniciará en cada actualización de actualización.

const login = (props) => {
  let _isMounted= true; // it isn't good because of a local variable, so the variable will be lost and re-initiated on every update render

  useEffect(() => {
    return () => {
        _isMounted = false;  // not good
    }
  }, []);

  // ...
}

Entonces, con la combinación de useRef y useEffect , podríamos limpiar completamente las pérdidas de memoria.


Los buenos enlaces que podría leer más sobre React Hooks son:

[ES] https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb

[FR] https://blog.soat.fr/2019/11/react-hooks-par-lexemple/

SanjiMika
fuente
1
Esto funcionó. Más tarde hoy leeré el enlace provisto para saber cómo esto resuelve el problema. Si pudiera dar más detalles sobre la respuesta para incluir los detalles, sería genial, por lo que será útil para los demás y también para otorgarle la recompensa después del período de gracia. Gracias.
Kenny Horna
Gracias por aceptar mi respuesta. Pensaré en tu solicitud y lo haré mañana.
SanjiMika
0

Puede utilizar el método 'cancelActiveVisits' Inertiapara cancelar el enlace activo visiten la useEffectlimpieza.

Entonces, con esta llamada, el activo visitse cancelará y el estado no se actualizará.

useEffect(() => {
    return () => {
        Inertia.cancelActiveVisits(); //To cancel the active visit.
    }
}, []);

Si la Inertiasolicitud se cancela, devolverá una respuesta vacía, por lo que debe agregar una verificación adicional para manejar la respuesta vacía. Agregue también agregar bloque de captura para manejar cualquier error potencial.

 function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
      .then(data => {
         if(data) {
            setLoading(false);
         }
      })
      .catch( error => {
         console.log(error);
      });
  }

Forma alternativa (solución alternativa)

Puede usar useRefpara mantener el estado del componente y, en función de esto, puede actualizar el state.

Problema:

La guerra se muestra porque handleSubmitestá intentando actualizar el estado del componente a pesar de que el componente se ha desmontado de la dom.

Solución:

Establezca una bandera para mantener el estado de la component, si componentes mountedentonces el flagvalor será truey si componentes unmountedel valor de la bandera será falso. Entonces, en base a esto, podemos actualizar el state. Para el estado de la bandera podemos usar useRefpara mantener una referencia.

useRefdevuelve un objeto de referencia mutable cuya .currentpropiedad se inicializa al argumento pasado (initialValue). El objeto devuelto persistirá durante toda la vida útil del componente. A useEffectcambio, una función que establecerá el estado del componente, si está desmontado.

Y luego, en useEffectla función de limpieza, podemos establecer el indicador enfalse.

Función de limpieza useEffecr

El useEffectgancho permite usar una función de limpieza. Cada vez que el efecto ya no es válido, por ejemplo, cuando un componente que usa ese efecto se desmonta, se llama a esta función para limpiar todo. En nuestro caso, podemos establecer la bandera en falso.

Ejemplo:

let _componentStatus.current =  useRef(true);
useEffect(() => {
    return () => {
        _componentStatus.current = false;
    }
}, []);

Y en handleSubmit podemos verificar si el componente está montado o no y actualizar el estado en función de esto.

function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
        .then(() => {
            if (_componentStatus.current) {
                setLoading(false);
            } else {
                _componentStatus = null;
            }
        })
}

En caso contrario, establezca el _componentStatusvalor en nulo para evitar pérdidas de memoria.

Sohail
fuente
No funcionó: /
Kenny Horna
¿Podrías consolar el valor de ajaxCalladentro useEffect? y ver cuál es el valor
Sohail
Perdón por el retraso. Vuelve undefined. Lo agregué justo después delreturn () => {
Kenny Horna
He cambiado el código. Intente con el nuevo código.
Sohail
No diré que esta es una solución o la forma correcta de resolver este problema, pero esto eliminará la advertencia.
Sohail