Vazamentos de memória de limpeza em um componente desmontado nos ganchos de reação

19

Sou novo no uso do React, por isso pode ser realmente simples de conseguir, mas não consigo descobrir sozinho, mesmo tendo pesquisado. Perdoe-me se isso é muito idiota.

Contexto

Estou usando o Inertia.js com os adaptadores Laravel (back-end) e React (front-end). Se você não conhece a Inércia, basicamente:

O Inertia.js permite criar rapidamente aplicativos modernos React, Vue e Svelte de página única, usando controladores e roteamento clássicos do lado do servidor.

Questão

Estou fazendo uma página de login simples que possui um formulário que, quando enviado, executará uma solicitação POST para carregar a próxima página. Parece funcionar bem, mas em outras páginas o console mostra o seguinte aviso:

Aviso: Não é possível executar uma atualização do estado React em um componente desmontado. Isso não funciona, mas indica um vazamento de memória no seu aplicativo. Para corrigir, cancele todas as assinaturas e tarefas assíncronas em uma função de limpeza useEffect.

no login (criado por Inertia)

O código relacionado (simplifiquei para evitar linhas 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;

Agora, eu sei que tenho que fazer uma função de limpeza, porque a promessa da solicitação é o que está gerando esse aviso. Eu sei que devo usar, useEffectmas não sei como aplicá-lo neste caso. Já vi exemplo quando um valor muda, mas como fazê-lo em uma chamada desse tipo?

Desde já, obrigado.


Atualizar

Conforme solicitado, o código completo deste 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
fonte
@Sohail Adicionei o código completo do componente
Kenny Horna
Você tentou simplesmente remover o .then(() => {})?
Guerric P

Respostas:

22

Como é a chamada de promessa assíncrona, você deve usar uma variável ref mutável (com useRef) para verificar o componente já desmontado para o próximo tratamento da resposta assíncrona (evitando vazamentos de memória):

Aviso: Não é possível executar uma atualização do estado React em um componente desmontado.

Dois ganchos de reação que você deve usar neste caso: useRefe useEffect.

Com useRef, por exemplo, a variável mutável _isMountedé sempre apontada para a mesma referência na memória (não uma variável local)

useRef é o gancho obrigatório se for necessária uma variável mutável. Diferente das variáveis ​​locais, o React garante que a mesma referência seja retornada durante cada renderização. Se você quiser, é o mesmo com this.myVar no Componente de Classe

Exemplo:

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

Na mesma ocasião, deixe-me explicar mais informações sobre os ganchos de reação usados ​​aqui. Além disso, compararei React Hooks no Componente funcional (a versão React> 16.8) com LifeCycle no Class Component.

useEffect : a maioria dos efeitos colaterais acontece dentro do gancho. Exemplos de efeitos colaterais são: busca de dados, configuração de uma assinatura e alteração manual do DOM nos componentes do React. O useEffect substitui muitos LifeCycles na classe Component (componentDidMount, componentDidUpate, componentWillUnmount)

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

1) O comportamento padrão de useEffect é executado após a primeira renderização (como ComponentDidMount) e após cada renderização de atualização (como ComponentDidUpdate) se você não tiver dependências. É assim :useEffect(fnc);

2) Dar várias dependências para useEffect mudará seu ciclo de vida. Neste exemplo: useEffect será chamado uma vez após a primeira renderização e sempre que a contagem de alterações for alterada

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

   useEffect(fnc, [count]);
}

3) useEffect será executado apenas uma vez após a primeira renderização (como ComponentDidMount) se você colocar uma matriz vazia para dependência. É assim :useEffect(fnc, []);

4) Para evitar vazamentos de recursos, tudo deve ser descartado quando o ciclo de vida de um gancho terminar (como ComponentWillUnmount) . Por exemplo, com a matriz vazia de dependência, a função retornada será chamada após a desmontagem do componente. É assim :

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

useRef : retorna um objeto ref mutável cuja propriedade .current é inicializada no argumento passado (initialValue). O objeto retornado persistirá por toda a vida útil do componente.

Exemplo: com a pergunta acima, não podemos usar uma variável local aqui porque ela será perdida e reiniciada em cada renderização de atualização.

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

  // ...
}

Portanto, com a combinação de useRef e useEffect , poderíamos limpar completamente os vazamentos de memória.


Os bons links que você pode ler mais sobre os React Hooks são:

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

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

SanjiMika
fonte
11
Isso funcionou. Mais tarde, hoje vou ler o link fornecido para saber como isso resolve o problema. Se você pudesse elaborar uma resposta para incluir os detalhes, seria ótimo, por isso será útil para outras pessoas e também lhe concederá a recompensa após o período de carência. Obrigado.
Kenny Horna
Obrigado por sua aceitação da minha resposta. Vou pensar no seu pedido e fazê-lo amanhã.
SanjiMika 28/01
0

Você pode usar o método 'cancelActiveVisits' Inertiapara cancelar o ativo visitno useEffectgancho de limpeza.

Portanto, com esta chamada, o ativo visitserá cancelado e o estado não será atualizado.

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

se a Inertiasolicitação for cancelada, ela retornará uma resposta vazia, para que você precise adicionar uma verificação extra para lidar com a resposta vazia. Adicionar add catch block também para lidar com possíveis erros.

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

Maneira alternativa (solução alternativa)

Você pode usar useRefpara manter o status do componente e, com base nisso, pode atualizar o state.

Problema:

A guerra está sendo exibida porque handleSubmitestá tentando atualizar o estado do componente, mesmo que o componente tenha sido desmontado do dom.

Solução:

Defina um sinalizador para manter o status de component, se componentfor, mountedentão o flagvalor será truee se componentfor unmountedo valor do sinalizador será falso. Então, com base nisso, podemos atualizar o state. Para o status da bandeira, podemos usaruseRef para manter uma referência.

useRefretorna um objeto ref mutável cuja .currentpropriedade é inicializada no argumento passado (initialValue). O objeto retornado persistirá por toda a vida útil do componente. Em useEffecttroca, uma função que definirá o status do componente, se ele estiver desmontado.

E então, na useEffectfunção de limpeza, podemos definir o sinalizador parafalse.

função de limpeza useEffecr

o useEffect gancho permite o uso de uma função de limpeza. Sempre que o efeito não é mais válido, por exemplo, quando um componente que usa esse efeito é desmontado, essa função é chamada para limpar tudo. No nosso caso, podemos definir o sinalizador como false.

Exemplo:

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

E no handleSubmit, podemos verificar se o componente está montado ou não e atualizar o estado com base nisso.

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

Caso contrário, defina _componentStatuscomo null para evitar vazamentos de memória.

Sohail
fonte
Não funcionou: /
Kenny Horna 17/01
Você poderia consolar o valor de ajaxCalldentro useEffect. e veja qual é o valor
Sohail
Desculpe o atraso. Retorna undefined. Eu adicionei logo após oreturn () => {
Kenny Horna 17/01
Eu mudei o código. Tente o novo código.
Sohail
Não direi que essa é uma correção ou a maneira correta de resolver esse problema, mas isso removerá o aviso.
Sohail