Reactフックのマウントされていないコンポーネントでのメモリリークのクリーンアップ


19

私はReactを初めて使用するので、これを達成するのは非常に簡単かもしれませんが、いくつかの調査を行ったとしても、自分でそれを理解することはできません。これがばかげている場合、私を許してください。

環境

Laravel(バックエンド)アダプターとReact(フロントエンド)アダプターでInertia.jsを使用しています。あなたが慣性を知らないなら、それは基本的に:

Inertia.jsを使用すると、クラシックなサーバー側ルーティングとコントローラーを使用して、最新の単一ページのReact、Vue、Svelteアプリをすばやく構築できます。

問題

私は、送信されたときにPOSTリクエストを実行して次のページをロードするフォームを持つ単純なログインページを実行しています。正常に動作しているようですが、他のページではコンソールに次の警告が表示されます:

警告:マウント解除されたコンポーネントでReact状態の更新を実行できません。これは何もしませんが、アプリケーションのメモリリークを示しています。修正するには、useEffectクリーンアップ関数ですべてのサブスクリプションと非同期タスクをキャンセルします。

ログイン中(慣性により作成)

関連コード(関係のない行を避けるために簡略化しました):

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;

これで、リクエストの約束がこの警告を生成するものであるため、クリーンアップ機能を実行する必要があることがわかりました。私は使うべきuseEffectだと知っていますが、この場合それをどのように適用するかわかりません。値が変化したときの例を見ましたが、この種の呼び出しでどのように行うのですか?

前もって感謝します。


更新

リクエストに応じて、このコンポーネントの完全なコード:

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コンポーネントの完全なコードを追加しました
Kenny Horna

単に削除しようとしました.then(() => {})か?
ゲリックP

回答:


22

これは非同期promise呼び出しであるため、(useRefを使用して)可変のref変数を使用して非同期応答の次の処理(メモリリークを回避する)のためにすでにマウントされていないコンポーネントを確認する必要があります。

警告:マウント解除されたコンポーネントでReact状態の更新を実行できません。

この場合に使用する必要がある2つのReactフック:useRefおよびuseEffect

useRef例えば、可変変数が_isMounted常にメモリ内の同じ基準で指摘されている(いないローカル変数)

可変変数が必要な場合は、useRef重要なフックです。ローカル変数とは異なり、Reactは各レンダリング中に同じ参照が確実に返されるようにします。必要に応じて、クラスコンポーネントのthis.myVarと同じです。

例:

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

同じ機会に、ここで使用されているReactフックについて詳しく説明します。また、機能コンポーネントのReactフック(バージョンReact> 16.8)とクラスコンポーネントのLifeCycleを比較します。

useEffect:ほとんどの副作用はフック内で発生します。副作用の例としては、データのフェッチ、サブスクリプションのセットアップ、ReactコンポーネントのDOMの手動変更などがあります。useEffectは、クラスコンポーネント(componentDidMount、componentDidUpate、componentWillUnmount)の多くのライフサイクルを置き換えます

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

1)依存関係がない場合、useEffectのデフォルトの動作は、最初のレンダリング(ComponentDidMountなど)の後、およびすべての更新レンダリング(ComponentDidUpdateなど)後に実行されます。そのようなものです :useEffect(fnc);

2)useEffectに一連の依存関係を与えると、そのライフサイクルが変わります。この例では、useEffectは最初のレンダリング後に1回、カウントが変更されるたびに呼び出されます

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

   useEffect(fnc, [count]);
}

3)依存関係に空の配列を配置した場合、useEffectは最初のレンダリング(ComponentDidMountなど)の後で1回だけ実行されます。そのようなものです :useEffect(fnc, []);

4)リソースのリークを防ぐには、フックのライフサイクルが終了したときにすべてを破棄する必要があります(ComponentWillUnmountなど)。たとえば、依存関係の空の配列では、コンポーネントがマウント解除された後に、返される関数が呼び出されます。そのようなものです :

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

useRef.currentプロパティが渡された引数(initialValue)に初期化されている変更可能なrefオブジェクトを返します。返されたオブジェクトは、コンポーネントの存続期間中存続します。

例:上記の質問では、ローカル変数はここでは使用できません。ローカル変数は、更新レンダリングのたびに失われ、再起動されるためです。

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

  // ...
}

したがって、useRefuseEffectを組み合わせることで、メモリリークを完全にクリーンアップできます。


React Hooksの詳細については、次のリンクをご覧ください。

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

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


1
これはうまくいきました。今日は、提供されているリンクを読んで、これによって問題がどのように解決されるかを実際に確認します。詳細を含めるために応答について詳しく説明できればすばらしいので、他の人にとっても、猶予期間後に賞金を授与することにも役立ちます。ありがとうございました。
ケニーホーナ

私の答えを受け入れてくれてありがとう。明日はあなたの要望を考えてやります。
SanjiMika

0

あなたはの「cancelActiveVisits」メソッドを使用することができInertia、アクティブをキャンセルするvisitにはuseEffect、クリーンアップフック。

したがって、この呼び出しでは、アクティブvisitはキャンセルされ、状態は更新されません。

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

Inertiaリクエストがキャンセルされた場合、空のレスポンスを返すため、空のレスポンスを処理するために追加のチェックを追加する必要があります。潜在的なエラーを処理するために、catchブロックも追加します。

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

別の方法(回避策)

を使用useRefしてコンポーネントのステータスを保持し、これに基づいてを更新できますstate

問題:

handleSubmitコンポーネントがdomからアンマウントされているにもかかわらず、がコンポーネントの状態を更新しようとしているため、警告が表示されています。

解決:

状態を保持するためのフラグを設定しcomponentた場合、componentあるmountedその後、flag値がされるtrueとあればcomponentあるunmountedフラグ値がfalseになります。したがって、これに基づいてを更新できstateます。フラグのステータスについてはuseRef、参照を保持するために使用できます。

useRef.current渡された引数(initialValue)にプロパティが初期化される変更可能なrefオブジェクトを返します。返されたオブジェクトは、コンポーネントの存続期間中存続します。でuseEffect、それがマウントされている場合、コンポーネントのステータスを設定する関数を返します。

そしてuseEffect、クリーンアップ関数で、フラグをfalse.

useEffecrクリーンアップ関数

useEffectフックがクリーンアップ機能を使用できます。たとえば、そのエフェクトを使用しているコンポーネントがマウント解除されている場合など、エフェクトが無効になると、この関数が呼び出されてすべてがクリーンアップされます。この例では、フラグをfalseに設定できます。

例:

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

そしてhandleSubmitで、コンポーネントがマウントされているかどうかを確認し、これに基づいて状態を更新できます。

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

それ以外の_componentStatus場合は、メモリリークを回避するためにをnullに設定します。


うまく
いき

ajaxCallinside の値をコンソールアウトできますかuseEffect。そして、その値が何であるかを見てください
Sohail

遅れて申し訳ありません。戻るundefined。直後に追加しましたreturn () => {
ケニーホーナ

コードを変更しました。新しいコードをお試しください。
ソハイル

これが修正またはこの問題を解決する正しい方法であるとは言いませんが、これにより警告が削除されます。
ソハイル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.