Fuites de mémoire de nettoyage sur un composant non monté dans les crochets React

19

Je suis nouveau avec React, donc cela peut être très simple à réaliser, mais je ne peux pas le comprendre par moi-même même si j'ai fait des recherches. Pardonnez-moi si c'est trop stupide.

Le contexte

J'utilise Inertia.js avec les adaptateurs Laravel (backend) et React (front-end). Si vous ne connaissez pas l'inertie, c'est essentiellement:

Inertia.js vous permet de créer rapidement des applications modernes React, Vue et Svelte sur une seule page à l'aide d'un routage et de contrôleurs côté serveur classiques.

Problème

Je fais une simple page de connexion qui a un formulaire qui, une fois soumis, exécutera une demande POST pour charger la page suivante. Cela semble fonctionner correctement, mais dans d'autres pages, la console affiche l'avertissement suivant:

Avertissement: impossible d'effectuer une mise à jour de l'état React sur un composant non monté. Il s'agit d'un no-op, mais cela indique une fuite de mémoire dans votre application. Pour résoudre ce problème, annulez tous les abonnements et les tâches asynchrones dans une fonction de nettoyage useEffect.

en connexion (créé par Inertia)

Le code associé (je l'ai simplifié pour éviter les lignes non pertinentes):

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;

Maintenant, je sais que je dois faire une fonction de nettoyage car la promesse de la demande est ce qui génère cet avertissement. Je sais que je devrais utiliseruseEffect mais je ne sais pas comment l'appliquer dans ce cas. J'ai vu un exemple quand une valeur change, mais comment le faire dans un appel de ce genre?

Merci d'avance.


Mise à jour

Comme demandé, le code complet de ce composant:

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
la source
@Sohail J'ai ajouté le code complet du composant
Kenny Horna
Avez-vous essayé de supprimer simplement le .then(() => {})?
Guerric P

Réponses:

22

Parce que c'est l'appel de promesse asynchrone, vous devez donc utiliser une variable ref mutable (avec useRef) pour vérifier le composant déjà non monté pour le prochain traitement de la réponse asynchrone (en évitant les fuites de mémoire):

Avertissement: impossible d'effectuer une mise à jour de l'état React sur un composant non monté.

Deux crochets React que vous devez utiliser dans ce cas: useRefetuseEffect .

Avec useRef, par exemple, la variable mutable _isMountedpointe toujours vers la même référence en mémoire (pas une variable locale)

useRef est le hook incontournable si une variable mutable est nécessaire. Contrairement aux variables locales, React s'assure que la même référence est renvoyée lors de chaque rendu. Si vous voulez, c'est la même chose avec this.myVar dans Class Component

Exemple :

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

À la même occasion, permettez-moi de vous expliquer plus d'informations sur les crochets React utilisés ici. Je comparerai également les crochets React dans le composant fonctionnel (la version React> 16.8) avec le composant LifeCycle in Class.

useEffect : la plupart des effets secondaires se produisent à l'intérieur du crochet. Exemples d'effets secondaires: récupération de données, configuration d'un abonnement et modification manuelle du DOM dans les composants React. UseEffect remplace de nombreux LifeCycles dans le composant de classe (componentDidMount, componentDidUpate, componentWillUnmount)

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

1) Le comportement par défaut de useEffect s'exécute à la fois après le premier rendu (comme ComponentDidMount) et après chaque rendu de mise à jour (comme ComponentDidUpdate) si vous n'avez pas de dépendances. C'est comme ça :useEffect(fnc);

2) Donner un tableau de dépendances à useEffect changera son cycle de vie. Dans cet exemple: useEffect sera appelé une fois après le premier rendu et à chaque changement de compte

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

   useEffect(fnc, [count]);
}

3) useEffect ne s'exécutera qu'une seule fois après le premier rendu (comme ComponentDidMount) si vous mettez un tableau vide pour la dépendance. C'est comme ça :useEffect(fnc, []);

4) Pour éviter les fuites de ressources, tout doit être éliminé à la fin du cycle de vie d'un crochet (comme ComponentWillUnmount) . Par exemple, avec le tableau de dépendances vide, la fonction retournée sera appelée après le démontage du composant. C'est comme ça :

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

useRef : retourne un objet ref mutable dont la propriété .current est initialisée à l'argument passé (initialValue). L'objet retourné persistera pendant toute la durée de vie du composant.

Exemple: avec la question ci-dessus, nous ne pouvons pas utiliser ici une variable locale car elle sera perdue et relancée à chaque rendu de mise à jour.

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

  // ...
}

Ainsi, avec la combinaison de useRef et useEffect , nous pourrions complètement nettoyer les fuites de mémoire.


Les bons liens que vous pourriez lire sur les crochets React sont:

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

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

SanjiMika
la source
1
Cela a fonctionné. Plus tard dans la journée, je vais lire le lien fourni pour savoir comment cela résout le problème. Si vous pouviez élaborer sur la réponse pour inclure les détails, ce serait bien, donc cela sera utile aux autres et aussi pour vous attribuer la prime après la période de grâce. Je vous remercie.
Kenny Horna
Merci d'avoir accepté ma réponse. Je réfléchirai à votre demande et le ferai demain.
SanjiMika
0

Vous pouvez utiliser la méthode 'cancelActiveVisits' Inertiapour annuler le hook actif visitdans le useEffectnettoyage.

Ainsi, avec cet appel, l'actif visitsera annulé et l'état ne sera pas mis à jour.

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

si la Inertiademande est annulée, elle retournera une réponse vide, vous devez donc ajouter une vérification supplémentaire pour gérer la réponse vide. Ajoutez également un bloc catch pour gérer les erreurs potentielles.

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

Autre moyen (solution de contournement)

Vous pouvez utiliser useRefpour conserver le statut du composant et sur cette base, vous pouvez mettre à jour le state.

Problème:

La guerre s'affiche parce que le handleSubmittente de mettre à jour l'état du composant même si le composant a été démonté du dom.

Solution:

Définissez un indicateur pour conserver le statut de component, si l' componentest mountedalors la flagvaleur sera trueet si l' componentest unmountedla valeur d'indicateur sera fausse. Donc, sur cette base, nous pouvons mettre à jour le state. Pour l'état du drapeau, nous pouvons utiliser useRefpour conserver une référence.

useRefrenvoie un objet ref mutable dont la .currentpropriété est initialisée à l'argument passé (initialValue). L'objet retourné persistera pendant toute la durée de vie du composant. En useEffectretour une fonction qui définira le statut du composant, s'il est démonté.

Et puis dans useEffectla fonction de nettoyage, nous pouvons définir le drapeau surfalse.

Fonction de nettoyage useEffecr

Le useEffectcrochet permet d'utiliser une fonction de nettoyage. Chaque fois que l'effet n'est plus valide, par exemple lorsqu'un composant utilisant cet effet est démonté, cette fonction est appelée pour tout nettoyer. Dans notre cas, nous pouvons définir le drapeau sur faux.

Exemple:

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

Et dans handleSubmit, nous pouvons vérifier si le composant est monté ou non et mettre à jour l'état en fonction de cela.

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

Sinon, définissez le _componentStatussur null pour éviter toute fuite de mémoire.

Sohail
la source
Cela n'a pas fonctionné: /
Kenny Horna
Pourriez-vous consoler la valeur de l' ajaxCallintérieur useEffect. et voyez quelle est la valeur
Sohail
Désolé pour le retard. Il revient undefined. Je l'ai ajouté juste après lereturn () => {
Kenny Horna
J'ai changé le code, veuillez essayer le nouveau code.
Sohail
Je ne dirai pas qu'il s'agit d'un correctif ou de la bonne façon de résoudre ce problème, mais cela supprimera l'avertissement.
Sohail