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

IT技术 javascript reactjs promise react-hooks inertiajs
2021-04-09 11:24:06

我是使用 React 的新手,所以这可能很容易实现,但即使我做了一些研究,我自己也无法弄清楚。如果这太愚蠢,请原谅我。

语境

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

Inertia.js 可让您使用经典的服务器端路由和控制器快速构建现代单页 React、Vue 和 Svelte 应用程序。

问题

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

警告:无法对卸载的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要修复,请取消 useEffect 清理函数中的所有订阅和异步任务。

在登录中(由 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;

现在,我知道我必须执行清理功能,因为请求的Promise是生成此警告的原因。我知道我应该使用,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;
5个回答

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

警告:无法对卸载的组件执行 React 状态更新。

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

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 Hooks 的更多信息。另外,我将比较函数式组件中的 React Hooks(React >16.8)和类组件中的 LifeCycle。

useEffect:大多数副作用发生在钩子内部。副作用的示例包括:数据获取、设置订阅和手动更改 DOM React 组件。useEffect 替换了 Class Component 中的很多 LifeCycles(componentDidMount、componentDidUpate、componentWillUnmount)

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

  2. 为 useEffect 提供依赖项数组将改变其生命周期。在这个例子中: useEffect 将在第一次渲染后调用一次,每次计数改变

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

    useEffect(fnc, [count]);
    

    }

  3. 如果您为依赖项放置一个空数组,则useEffect 将仅在第一次渲染后运行一次(如 ComponentDidMount)就像那样 :useEffect(fnc, []);

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

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

useRef:返回一个可变的 ref 对象,.current属性被初始化为传递的参数 (initialValue)。返回的对象将在组件的整个生命周期内持续存在。

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

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 的好链接是:

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

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

感谢您接受我的回答。我会考虑你的要求,明天再做。
2021-06-14 11:24:06
这奏效了。今天晚些时候,我将阅读提供的链接,以实际了解如何解决问题。如果您能详细说明回复以包含详细信息,那就太好了,这将对他人有所帮助,并在宽限期后向您授予赏金。谢谢你。
2021-06-20 11:24:06

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

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

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

你可以使用的“cancelActiveVisits”的方法Inertia来取消活动visituseEffect清理挂钩。

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

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

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

useRef返回一个可变的 ref 对象,其.current属性被初始化为传递的参数 (initialValue)。返回的对象将在组件的整个生命周期内持续存在。作为useEffect回报,一个函数将设置组件的状态,如果它被卸载。

然后在useEffect清理函数中我们可以将标志设置为false.

使用Effecr清理功能

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

在 else 中将 设置_componentStatus为 null 以避免任何内存泄漏。

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

当组件被卸载并且您调用setState您在其中执行的操作时会出现警告

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

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

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

基于函数和基于类的两个👇🏻



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

请添加更多详细信息以扩展您的答案,例如工作代码或文档引用。
2021-06-10 11:24:06