La memoria di pulizia perde su un componente smontato in React Hooks


19

Sto usando nuovo React, quindi questo potrebbe essere davvero semplice da raggiungere, ma non riesco a capirlo da solo, anche se ho fatto qualche ricerca. Perdonami se questo è troppo stupido.

Contesto

Sto usando Inertia.js con gli adattatori Laravel (backend) e React (front-end). Se non conosci l'inerzia, sostanzialmente:

Inertia.js ti consente di creare rapidamente moderne app React, Vue e Svelte a pagina singola utilizzando i classici routing e controller lato server.

Problema

Sto facendo una semplice pagina di accesso che ha un modulo che una volta inviato eseguirà una richiesta POST per caricare la pagina successiva. Sembra funzionare bene ma in altre pagine la console mostra il seguente avviso:

Avviso: impossibile eseguire un aggiornamento dello stato React su un componente non montato. Questa è una no-op, ma indica una perdita di memoria nell'applicazione. Per risolvere, annulla tutte le sottoscrizioni e le attività asincrone in una funzione di pulizia useEffect.

in login (creato da Inertia)

Il codice correlato (l'ho semplificato per evitare righe irrilevanti):

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;

Ora so che devo eseguire una funzione di pulizia perché la promessa della richiesta è ciò che sta generando questo avviso. So che dovrei usare useEffectma non so come applicarlo in questo caso. Ho visto degli esempi quando cambia un valore, ma come farlo in una chiamata di questo tipo?

Grazie in anticipo.


Aggiornare

Come richiesto, il codice completo di questo 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;

@Sohail Ho aggiunto il codice completo del componente
Kenny Horna il

Hai provato a rimuovere semplicemente il .then(() => {})?
Guerric P

Risposte:


22

Perché è la chiamata promessa asincrona, quindi è necessario utilizzare una variabile ref mutabile (con useRef) per controllare il componente già smontato per il successivo trattamento della risposta asincrona (evitando perdite di memoria):

Avviso: impossibile eseguire un aggiornamento dello stato React su un componente non montato.

Due ganci React che dovresti usare in questo caso: useRefe useEffect.

Con useRef, ad esempio, la variabile mutabile _isMountedè sempre puntata sullo stesso riferimento in memoria (non una variabile locale)

useRef è l'hook go-to se è necessaria una variabile mutabile. A differenza delle variabili locali, React si assicura che lo stesso riferimento venga restituito durante ogni rendering. Se vuoi, è lo stesso con this.myVar nel componente di classe

Esempio :

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

Nella stessa occasione, lascia che ti spieghi ulteriori informazioni sugli ami React usati qui. Inoltre, confronterò React Hooks in Functional Component (la versione React> 16.8) con LifeCycle in Class Component.

useEffect : la maggior parte degli effetti collaterali si verificano all'interno del gancio. Esempi di effetti collaterali sono: recupero dei dati, impostazione di un abbonamento e modifica manuale del DOM nei componenti di React. UseEffect sostituisce molti LifeCycles in Class Component (componentDidMount, componentDidUpate, componentWillUnmount)

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

1) Comportamento predefinito di useEffect viene eseguito sia dopo il primo rendering (come ComponentDidMount) sia dopo ogni rendering di aggiornamento (come ComponentDidUpdate) se non si hanno dipendenze. È come questo :useEffect(fnc);

2) Dare array di dipendenze da usareEffect cambierà il suo ciclo di vita. In questo esempio: useEffect verrà chiamato una volta dopo il primo rendering e ogni volta che il conteggio cambia

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

   useEffect(fnc, [count]);
}

3) useEffect verrà eseguito solo una volta dopo il primo rendering (come ComponentDidMount) se si inserisce un array vuoto per la dipendenza. È come questo :useEffect(fnc, []);

4) Per evitare perdite di risorse, tutto deve essere smaltito al termine del ciclo di vita di un hook (come ComponentWillUnmount) . Ad esempio, con l'array vuoto di dipendenza, la funzione restituita verrà chiamata dopo lo smontaggio del componente. È come questo :

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

useRef : restituisce un oggetto ref mutabile la cui proprietà .current è inizializzata sull'argomento passato (initialValue). L'oggetto restituito persisterà per l'intera durata del componente.

Esempio: con la domanda sopra, non possiamo usare qui una variabile locale perché andrà persa e riavviata ad ogni rendering di aggiornamento.

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

  // ...
}

Quindi, con la combinazione di useRef e useEffect , potremmo eliminare completamente le perdite di memoria.


I buoni link che potresti leggere di più sui React Hooks sono:

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

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


1
Questo ha funzionato. Più tardi oggi leggerò il link fornito per capire come questo risolve il problema. Se potessi approfondire la risposta per includere i dettagli sarebbe grandioso, quindi sarà utile per gli altri e anche per assegnare la grazia a te dopo il periodo di grazia. Grazie.
Kenny Horna,

Grazie per aver accettato la mia risposta. Penserò alla tua richiesta e lo farò domani.
SanjiMika,

0

È possibile utilizzare il metodo 'cancelActiveVisits' Inertiaper annullare l' hook attivo visitnel useEffectcleanup.

Quindi, con questa chiamata, l'attivo visitverrà annullato e lo stato non verrà aggiornato.

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

se la Inertiarichiesta viene annullata, verrà restituita una risposta vuota, pertanto è necessario aggiungere un controllo aggiuntivo per gestire la risposta vuota. Aggiungi anche il blocco catch per gestire eventuali errori.

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

Modo alternativo (soluzione alternativa)

È possibile utilizzare useRefper conservare lo stato del componente e in base a ciò è possibile aggiornare il state.

Problema:

La guerra sta mostrando perché handleSubmitsta cercando di aggiornare lo stato del componente anche se il componente ha smontato dalla dom.

Soluzione:

Impostare un flag per mantenere lo stato di component, se componentè mountedquindi il flagvalore sarà truee se componentè unmountedil valore del flag sarà falso. Quindi in base a questo possiamo aggiornare il state. Per lo stato della bandiera possiamo usare useRefper contenere un riferimento.

useRefrestituisce un oggetto ref mutabile la cui .currentproprietà è inizializzata sull'argomento passato (initialValue). L'oggetto restituito persisterà per l'intera durata del componente. In useEffectcambio una funzione che imposterà lo stato del componente, se smontato.

E poi nella useEffectfunzione di pulizia possiamo impostare il flag sufalse.

Funzione di pulizia useEffecr

Il useEffectgancio consente di utilizzare una funzione di pulizia. Ogni volta che l'effetto non è più valido, ad esempio quando un componente che utilizza quell'effetto è smontato, questa funzione viene chiamata per ripulire tutto. Nel nostro caso, possiamo impostare la bandiera su false.

Esempio:

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

E in handleSubmit possiamo verificare se il componente è montato o meno e aggiornare lo stato in base a questo.

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 imposta il valore _componentStatusnull per evitare perdite di memoria.


Non ha funzionato: /
Kenny Horna il

Potresti consolare il valore di ajaxCalldentro useEffect. e vedi qual è il valore
Sohail

Scusa per il ritardo. Ritorna undefined. L'ho aggiunto subito doporeturn () => {
Kenny Horna il

Ho cambiato il codice, prova il nuovo codice.
Sohail,

Non dirò che questa è una correzione o il modo corretto per risolvere questo problema, ma questo rimuoverà l'avvertimento.
Sohail,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.