清除 React Hooks 中未安装组件的内存泄漏

Cleanup memory leaks on an Unmounted Component in React Hooks

我是 React 的新手,所以实现起来可能真的很简单,但即使我做了一些研究,我也无法自己弄清楚。太蠢了还请见谅

上下文

我将 Inertia.js 与 Laravel(后端)和 React(前端)适配器一起使用。如果你不知道惯性,它基本上是:

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.

问题

我正在做一个简单的登录页面,该页面有一个表单,提交后将执行 POST 请求以加载下一页。它似乎工作正常,但在其他页面中,控制台显示以下警告:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

in login (created by Inertia)

相关代码(我已经简化了它以避免不相关的行):

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;

您可以使用 Inertia 的 'cancelActiveVisits' 方法取消 useEffect 清理挂钩中的活动 visit

因此,通过此调用,活动 visit 将被取消,状态将不会更新。

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

如果 Inertia 请求被取消,那么它将 return 一个空响应,因此您必须添加额外的检查来处理空响应。 添加添加 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 的状态,如果 componentmounted,那么 flag 的值将是 true,如果componentunmounted 标志值将为 false。所以基于此我们可以更新state。 对于标志状态,我们可以使用 useRef 来保存参考。

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. In useEffect return a function which will set the status of the component, if it is unmounted.

然后在清理函数中的useEffect我们可以设置flag为false.

使用Effecr清理功能

The useEffect hook allows using a cleanup function. Anytime the effect is no longer valid, for example when a component using that effect is unmounting, this function is called to clean everything up. In our case, we can set the flag to 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 以避免任何内存泄漏。

因为是异步的promise调用,所以你必须使用一个可变的引用变量(with useRef)来检查已经卸载的组件,以便下一次处理异步响应(避免内存泄漏) :

Warning: Can't perform a React state update on an unmounted component.

在这种情况下您应该使用的两个 React Hooks:useRefuseEffect.

useRef为例,可变变量_isMounted总是指向内存中同一个引用(不是局部变量)

useRef is the go-to hook if mutable variable is needed. Unlike local variables, React makes sure same reference is returned during each render. If you want, it's the same with this.myVar in Class Component

示例:

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 Hooks。此外,我将比较功能组件(React >16.8)中的 React Hooks 与 Class 组件中的 LifeCycle。

useEffect : Most side-effects happen inside the hook. Examples of side effects are : data fetching, setting up a subscription, and manually changing the DOM React components. The useEffect replaces a lot of LifeCycles in Class Component (componentDidMount, componentDidUpate, componentWillUnmount)

 useEffect(fnc, [dependency1, dependency2, ...]); // dependencies array argument is optional
  1. useEffect 运行s 的默认行为在第一次渲染后 (如 ComponentDidMount) 和每次更新渲染后 (像 ComponentDidUpdate) 如果你没有依赖项。是这样的:useEffect(fnc);

  2. 将依赖项数组提供给 useEffect 将改变其生命周期。在此示例中:useEffect 将在第一次渲染后调用一次,每次计数都会更改

    导出默认函数(){ const [count, setCount] = useState(0);

    useEffect(fnc, [count]);
    

    }

  3. useEffect 只会在第一次渲染后 运行 一次 (如 ComponentDidMount) 如果你为依赖项放置一个空数组。是这样的:useEffect(fnc, []);

  4. 为了防止资源泄漏,必须在挂钩的生命周期结束时处理所有内容(如 ComponentWillUnmount)。例如,对于空的依赖数组,returned 函数将在组件卸载后调用。是这样的:

    useEffect(() => { return fnc_cleanUp; // fnc_cleanUp 将取消所有订阅和异步任务(例如:clearInterval) }, []);

useRef : returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

示例:对于上面的问题,我们不能在这里使用局部变量,因为它会丢失并在每次更新渲染时重新启动。

const login = (props) => {
  let _isMounted= true; // it isn't good because of a local variable, so the variable will be lost and re-defined 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/

我知道我有点晚了,但有一个更简单的解决方案。将代码设计为在卸载后不使用状态。

当组件被卸载并且您调用 setState 时出现警告,您在

 .then(() => {
   // Warning : memory leaks during the state update on the unmounted component <--------
   setLoading(false);
 }) 

你并不真正需要,因为如果组件已经卸载,就会发生这种情况。因此,如果您只是将其删除,则不会收到警告。

所以我对现在阅读本文的任何人的建议是,尝试重构代码以在调用卸载组件的函数后不设置状态。

在改变状态之前,你应该首先检查组件是否仍然挂载。

正如上面 所说,当有导致此错误的异步操作时, 这意味着您正在尝试在组件卸载后改变组件的状态。

react-use 提供了钩子,你有两个选择:

选项 #1:useMountedState

// check if isMounted before changing any state
const isMounted = useMountedState();

useEffect(() => {
  const asyncAction = executeAsyncAction();

  asyncAction.then(result => {
    if (isMounted) {
      // It's safe to mutate state here
    }
  });
}, []);

选项#2:useUnmountPromise

/* `resolveWhileMounted` wraps your promise, and returns a promise that will resolve 
 * only while the component is still mounted */
const resolveWhileMounted = useUnmountPromise();

useEffect(async () => {
  const asyncAction = executeAsyncAction();

  resolveWhileMounted(asyncAction).then(result => {
    // It's safe to mutate state here
  });
}, []);

基于功能和 Class 同时基于



 // function based METHOD 1 
const unsubscribe = useRef();
useEffect(() => {
  unsubscribe.current = setTimeout(() => {
    // do something
  }, 1000);
  return () => {
    clearTimeout(unsubscribe.current);
    unsubscribe.current = null;
  };
}, []);



// function based METHOD 2 
const [news, setNews] = useState();
const unsubscribe = useRef();
useEffect(() => {
  unsubscribe.current = true
  axios.get('domain').then((result) => {
    if (unsubscribe) {
      setNews(result);
    }
  });
  return () => {
    unsubscribe.current = false;
  };
}, []);

 

// class based METHOD 1 
  unsubscribe = null;
  componentDidMount() {
    this.unsubscribe = setTimeout(() => {
      // do something
    }, 1000);
  }
  componentWillUnmount() {
    clearTimeout(this.unsubscribe);
    this.unsubscribe = null;
  } 




// class based METHOD 4
unsubscribe = false;
  constructor(props) {
    super(props);
    this.state = {
      news: [],
    };
  }
  componentDidMount() {
    this.unsubscribe = true;
    axios.get('domain').then((result) => {
      if (this.unsubscribe) {
        this.setState({
          news: result.data.hits,
        });
      }
    });
  }
  componentWillUnmount() {
    this.unsubscribe = false;
  }