Bereinigungsspeicherlecks an einer nicht gemounteten Komponente in React Hooks

19

Ich bin neu in der Verwendung von React, daher ist dies möglicherweise sehr einfach zu erreichen, aber ich kann es nicht selbst herausfinden, obwohl ich einige Nachforschungen angestellt habe. Vergib mir, wenn das zu dumm ist.

Kontext

Ich verwende Inertia.js mit den Adaptern Laravel (Backend) und React (Frontend). Wenn Sie Trägheit nicht kennen, ist es im Grunde:

Mit Inertia.js können Sie schnell moderne einseitige React-, Vue- und Svelte-Apps mit klassischem serverseitigem Routing und Controllern erstellen.

Problem

Ich mache eine einfache Anmeldeseite mit einem Formular, das beim Senden eine POST-Anforderung zum Laden der nächsten Seite ausführt. Es scheint gut zu funktionieren, aber auf anderen Seiten zeigt die Konsole die folgende Warnung:

Warnung: Es kann keine Aktualisierung des Reaktionsstatus für eine nicht gemountete Komponente durchgeführt werden. Dies ist ein No-Op, weist jedoch auf einen Speicherverlust in Ihrer Anwendung hin. Brechen Sie zum Beheben alle Abonnements und asynchronen Aufgaben in einer useEffect-Bereinigungsfunktion ab.

im Login (erstellt von Inertia)

Der zugehörige Code (ich habe ihn vereinfacht, um irrelevante Zeilen zu vermeiden):

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;

Jetzt weiß ich, dass ich eine Bereinigungsfunktion ausführen muss, da das Versprechen der Anforderung das ist, was diese Warnung generiert. Ich weiß, dass ich es verwenden sollte, useEffectaber ich weiß nicht, wie ich es in diesem Fall anwenden soll. Ich habe ein Beispiel gesehen, wenn sich ein Wert ändert, aber wie geht das bei einem Aufruf dieser Art?

Danke im Voraus.


Aktualisieren

Wie gewünscht, der vollständige Code dieser Komponente:

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
quelle
@Sohail Ich habe den vollständigen Code der Komponente hinzugefügt
Kenny Horna
Haben Sie versucht, das einfach zu entfernen .then(() => {})?
Guerric P

Antworten:

22

Da es sich um den asynchronen Versprechungsaufruf handelt, müssen Sie eine veränderbare Referenzvariable (mit useRef) verwenden , um bereits nicht gemountete Komponenten auf die nächste Behandlung der asynchronen Antwort zu überprüfen (um Speicherlecks zu vermeiden):

Warnung: Es kann keine Aktualisierung des Reaktionsstatus für eine nicht gemountete Komponente durchgeführt werden.

Zwei Reaktionshaken, die Sie in diesem Fall verwenden sollten: useRefund useEffect.

Mit useRef, beispielsweise die veränderliche Variable _isMountedist , immer an der gleichen Referenz im Speicher zeigt (nicht eine lokale Variable)

useRef ist der Go-to-Hook, wenn eine veränderbare Variable benötigt wird. Im Gegensatz zu lokalen Variablen stellt React sicher, dass bei jedem Rendern dieselbe Referenz zurückgegeben wird. Wenn Sie möchten, ist es mit this.myVar in Class Component dasselbe

Beispiel:

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

Lassen Sie mich Ihnen bei der gleichen Gelegenheit weitere Informationen zu den hier verwendeten React Hooks erläutern. Außerdem werde ich React Hooks in Functional Component (die Version React> 16.8) mit LifeCycle in Class Component vergleichen.

useEffect : Die meisten Nebenwirkungen treten im Hook auf. Beispiele für Nebenwirkungen sind: Datenabruf, Einrichten eines Abonnements und manuelles Ändern des DOM in React-Komponenten. Der useEffect ersetzt viele LifeCycles in Class Component (componentDidMount, componentDidUpate, componentWillUnmount).

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

1) Das Standardverhalten von useEffect wird sowohl nach dem ersten Rendern (wie ComponentDidMount) als auch nach jedem Update-Rendering (wie ComponentDidUpdate) ausgeführt, wenn Sie keine Abhängigkeiten haben. Es ist wie es ist :useEffect(fnc);

2) Wenn Sie eine Reihe von Abhängigkeiten für useEffect angeben, ändert sich der Lebenszyklus. In diesem Beispiel: useEffect wird nach dem ersten Rendern einmal aufgerufen und jedes Mal, wenn sich die Anzahl ändert

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

   useEffect(fnc, [count]);
}

3) useEffect wird nach dem ersten Rendern nur einmal ausgeführt (wie ComponentDidMount), wenn Sie ein leeres Array für die Abhängigkeit . Es ist wie es ist :useEffect(fnc, []);

4) Um Ressourcenlecks zu vermeiden, muss alles entsorgt werden, wenn der Lebenszyklus eines Hooks endet (wie bei ComponentWillUnmount) . Beispielsweise wird mit dem leeren Abhängigkeitsarray die zurückgegebene Funktion aufgerufen, nachdem die Komponenten ausgehängt wurden. Es ist wie es ist :

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

useRef : Gibt ein veränderbares ref-Objekt zurück, dessen .current Eigenschaft mit dem übergebenen Argument (initialValue) initialisiert wird. Das zurückgegebene Objekt bleibt für die gesamte Lebensdauer der Komponente bestehen.

Beispiel: Bei der obigen Frage können wir hier keine lokale Variable verwenden, da diese bei jedem Update-Rendering verloren geht und neu initiiert wird.

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

  // ...
}

Mit der Kombination von useRef und useEffect können wir also Speicherlecks vollständig bereinigen.


Die guten Links, über die Sie mehr über die React Hooks lesen können, sind:

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

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

SanjiMika
quelle
1
Das hat funktioniert. Später heute werde ich den bereitgestellten Link lesen, um zu erfahren, wie dies das Problem löst. Wenn Sie die Antwort näher erläutern könnten, um die Details aufzunehmen, wäre dies großartig. Es wäre also hilfreich für andere und auch, Ihnen das Kopfgeld nach der Nachfrist zu gewähren. Vielen Dank.
Kenny Horna
Vielen Dank für Ihre Annahme meiner Antwort. Ich werde über Ihre Anfrage nachdenken und es morgen tun.
SanjiMika
0

Sie können die Methode 'cancelActiveVisits' verwenden Inertia, um den aktiven Hook visitfür die useEffectBereinigung abzubrechen .

Mit diesem Anruf wird der aktive visitVorgang abgebrochen und der Status wird nicht aktualisiert.

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

Wenn die InertiaAnforderung abgebrochen wird, wird eine leere Antwort zurückgegeben, sodass Sie eine zusätzliche Prüfung hinzufügen müssen, um die leere Antwort zu verarbeiten. Fügen Sie auch add catch catch-Block hinzu, um mögliche Fehler zu behandeln.

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

Alternativer Weg (Problemumgehung)

Sie können verwenden useRef, um den Status der Komponente zu speichern, und auf dieser Grundlage können Sie die aktualisieren state.

Problem:

Der Warring wird angezeigt, weil handleSubmitversucht wird, den Status der Komponente zu aktualisieren, obwohl die Komponente vom Dom nicht bereitgestellt wurde.

Lösung:

Setzen Sie ein Flag, um den Status von zu speichern. componentWenn das componentist, mountedist der flagWert trueund wenn das componentist, ist unmountedder Flag-Wert falsch. Auf dieser Grundlage können wir das aktualisieren state. Für den Flaggenstatus können wir useRefeine Referenz halten.

useRefGibt ein veränderbares ref-Objekt zurück, dessen .currentEigenschaft mit dem übergebenen Argument (initialValue) initialisiert wird. Das zurückgegebene Objekt bleibt für die gesamte Lebensdauer der Komponente bestehen. Im useEffectGegenzug eine Funktion, die den Status der Komponente festlegt, wenn sie nicht gemountet ist.

Und dann useEffectkönnen wir in der Bereinigungsfunktion das Flag auf setzenfalse.

useEffecr Bereinigungsfunktion

Der useEffectHaken ermöglicht die Verwendung einer Bereinigungsfunktion. Immer wenn der Effekt nicht mehr gültig ist, z. B. wenn eine Komponente, die diesen Effekt verwendet, die Bereitstellung aufhebt, wird diese Funktion aufgerufen, um alles zu bereinigen. In unserem Fall können wir das Flag auf false setzen.

Beispiel:

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

Und in handleSubmit können wir prüfen, ob die Komponente gemountet ist oder nicht, und den Status basierend darauf aktualisieren.

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

In else setzen Sie den _componentStatusWert auf null, um Speicherverluste zu vermeiden.

Sohail
quelle
Es hat nicht funktioniert: /
Kenny Horna
Könnten Sie den Wert von ajaxCallinnen trösten useEffect. und sehen, was der Wert ist
Sohail
Entschuldigung für die Verspätung. Es kehrt zurück undefined. Ich habe es kurz nach demreturn () => {
Kenny Horna
Ich habe den Code geändert. Bitte versuchen Sie es mit dem neuen Code.
Sohail
Ich werde nicht sagen, dass dies ein Fix oder der richtige Weg ist, um dieses Problem zu lösen, aber dadurch wird die Warnung entfernt.
Sohail