Wyciek pamięci pamięci na niezmontowanym komponencie w hakach reagujących

19

Jestem nowy, używając React, więc może to być naprawdę łatwe do osiągnięcia, ale nie mogę tego sam zrozumieć, chociaż przeprowadziłem pewne badania. Wybacz mi, jeśli to jest zbyt głupie.

Kontekst

Używam Inertia.js z adapterami Laravel (backend) i React (front-end). Jeśli nie znasz bezwładności, to w zasadzie:

Inertia.js pozwala szybko tworzyć nowoczesne jednostronicowe aplikacje React, Vue i Svelte przy użyciu klasycznego routingu i kontrolerów po stronie serwera.

Kwestia

Robię prostą stronę logowania, która ma formularz, który po przesłaniu wykona żądanie POST, aby załadować następną stronę. Wygląda na to, że działa dobrze, ale na innych stronach konsola wyświetla następujące ostrzeżenie:

Ostrzeżenie: Nie można wykonać aktualizacji stanu React dla odmontowanego komponentu. To nie działa, ale oznacza wyciek pamięci w Twojej aplikacji. Aby to naprawić, anuluj wszystkie subskrypcje i zadania asynchroniczne w funkcji czyszczenia useEffect.

w login (stworzony przez Inertia)

Powiązany kod (uprościłem go, aby uniknąć niepotrzebnych wierszy):

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;

Teraz wiem, że muszę wykonać funkcję czyszczenia, ponieważ obietnica żądania generuje to ostrzeżenie. Wiem, że powinienem użyć, useEffectale nie wiem, jak go zastosować w tym przypadku. Widziałem przykład zmiany wartości, ale jak to zrobić w tego rodzaju połączeniu?

Z góry dziękuję.


Aktualizacja

Zgodnie z życzeniem, pełny kod tego komponentu:

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
źródło
@Siłownik Dodałem pełny kod komponentu
Kenny Horna
Czy próbowałeś po prostu usunąć .then(() => {})?
Guerric P

Odpowiedzi:

22

Ponieważ jest to wywołanie obietnicy asynchronicznej, musisz więc użyć zmiennej zmiennej ref (with useRef), aby sprawdzić już odmontowany komponent do następnego przetwarzania odpowiedzi asynchronicznej (unikanie przecieków pamięci):

Ostrzeżenie: Nie można wykonać aktualizacji stanu React dla odmontowanego komponentu.

Dwa zaczepy reakcyjne, których należy użyć w tym przypadku: useRefi useEffect.

Z useRef, na przykład, zmienna zmienny _isMountedjest zawsze skierowany na samego odniesienia w pamięci (nie jest to zmienna lokalna)

useRef jest punktem zaczepienia, jeśli potrzebna jest zmienna zmienna. W przeciwieństwie do zmiennych lokalnych, React zapewnia, że ​​to samo odwołanie jest zwracane podczas każdego renderowania. Jeśli chcesz, to samo z tym. MyVar w Class Component

Przykład:

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

Przy tej samej okazji pozwól, że wyjaśnię ci więcej informacji na temat używanych haków React. Porównuję także React Hooks w Functional Component (wersja React> 16.8) z LifeCycle in Class Component.

useEffect : Większość efektów ubocznych występuje wewnątrz haka. Przykładami efektów ubocznych są: pobieranie danych, konfiguracja subskrypcji i ręczna zmiana DOM w komponentach React. UseEffect zastępuje wiele LifeCycle w klasie Component (componentDidMount, componentDidUpate, componentWillUnmount)

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

1) Domyślne zachowanie useEffect działa zarówno po pierwszym renderowaniu (jak ComponentDidMount), jak i po każdym renderowaniu aktualizacji (jak ComponentDidUpdate), jeśli nie masz zależności. To tak:useEffect(fnc);

2) Podanie tablicy zależności do użyciaEffect zmieni jego cykl życia. W tym przykładzie: useEffect zostanie wywołany jeden raz po pierwszym renderowaniu i za każdym razem zmieni się liczenie

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

   useEffect(fnc, [count]);
}

3) useEffect uruchomi się tylko raz po pierwszym renderowaniu (jak ComponentDidMount), jeśli umieścisz pustą tablicę dla zależności. To tak:useEffect(fnc, []);

4) Aby zapobiec wyciekom zasobów, wszystko musi zostać usunięte po zakończeniu cyklu życia haka (np. ComponentWillUnmount) . Na przykład przy pustej tablicy zależności zwracana funkcja zostanie wywołana po odmontowaniu komponentu. To tak:

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

useRef : zwraca się obiekt zmienny ref którego .aktualnie właściwość jest inicjowane przechodzi argumentu (InitialValue). Zwrócony obiekt będzie trwał przez cały okres istnienia komponentu.

Przykład: przy powyższym pytaniu nie możemy użyć zmiennej lokalnej, ponieważ zostanie ona utracona i ponownie zainicjowana przy każdym renderowaniu aktualizacji.

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

  // ...
}

Tak więc, dzięki kombinacji useRef i useEffect , możemy całkowicie usunąć przecieki pamięci.


Dobre linki, które można przeczytać więcej o hakach React, to:

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

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

SanjiMika
źródło
1
To zadziałało. Później dzisiaj przeczytam podany link, aby dowiedzieć się, jak to rozwiązać problem. Jeśli mógłbyś rozwinąć odpowiedź na temat dołączenia szczegółów, byłoby świetnie, więc będzie to pomocne dla innych, a także przyznanie nagrody po okresie karencji. Dziękuję Ci.
Kenny Horna
Dziękuję za zaakceptowanie mojej odpowiedzi. Pomyślę o twojej prośbie i zrobię to jutro.
SanjiMika
0

Możesz użyć metody „cancelActiveVisits”, Inertiaaby anulować aktywny visitw useEffecthaku czyszczenia.

Dzięki temu połączeniu aktywne visitzostaną anulowane i stan nie zostanie zaktualizowany.

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

jeśli Inertiażądanie zostanie anulowane, zwróci pustą odpowiedź, więc musisz dodać dodatkowe sprawdzenie, aby obsłużyć pustą odpowiedź. Dodaj również blok catch, aby obsłużyć wszelkie potencjalne błędy.

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

Alternatywny sposób (obejście)

Możesz użyć useRefdo utrzymania statusu komponentu i na tej podstawie możesz zaktualizować state.

Problem:

Walka jest wyświetlana, ponieważ handleSubmitpróbuje zaktualizować stan komponentu, mimo że komponent został odmontowany z domeny.

Rozwiązanie:

Ustawić flagę trzymać stanu połączenia component, jeżeli componentjest mountedto flagwartość będzie true, a jeśli componentjest unmountedwartość flagi będą fałszywe. Na tej podstawie możemy zaktualizować state. Dla statusu flagi możemy użyć useRefdo przechowywania referencji.

useRefzwraca zmienny obiekt referencyjny, którego .currentwłaściwość jest zainicjowana do przekazanego argumentu (initialValue). Zwrócony obiekt będzie trwał przez cały okres istnienia komponentu. W useEffectzamian funkcja, która ustawi status komponentu, jeśli jest on odmontowany.

A następnie w useEffectfunkcji czyszczenia możemy ustawić flagę nafalse.

useEffecr funkcja czyszczenia

useEffectHak pozwala przy użyciu funkcji czyszczenia. Za każdym razem, gdy efekt przestaje być ważny, na przykład gdy składnik korzystający z tego efektu odmontowuje się, funkcja ta jest wywoływana, aby wszystko wyczyścić. W naszym przypadku możemy ustawić flagę na false.

Przykład:

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

I w handleSubmit możemy sprawdzić, czy komponent jest zamontowany czy nie, i zaktualizować stan na tej podstawie.

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

W innym przypadku ustaw _componentStatuswartość null, aby uniknąć wycieków pamięci.

Sohail
źródło
Nie działało: /
Kenny Horna
Czy możesz pocieszyć wartość ajaxCallwnętrza useEffect. i zobacz, jaka jest wartość
Sohail
Przepraszam za opóźnienie. Powraca undefined. Dodałem go zaraz poreturn () => {
Kenny Horna
Zmieniłem kod. Proszę spróbować nowego kodu.
Sohail,
Nie powiem, że jest to poprawka lub właściwy sposób rozwiązania tego problemu, ale usunie to ostrzeżenie.
Sohail,