如何在useEffect钩子反应中停止内存泄漏

Posted

技术标签:

【中文标题】如何在useEffect钩子反应中停止内存泄漏【英文标题】:How to stop memory leak in useEffect hook react 【发布时间】:2020-01-22 01:57:29 【问题描述】:

我正在使用效果挂钩从服务器获取数据,这些数据被传递到反应表,我使用相同的 api 调用从服务器加载下一组数据。 当应用程序被加载时,我收到如下警告

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.

效果挂钩:

useEffect(() => 
setPageLoading(true);
props
  .dispatch(fetchCourses())
  .then(() => 
    setPageLoading(false);
  )
  .catch((error: string) => 
    toast.error(error);
    setPageLoading(false);
  );
, []);

反应表页面:

<ReactTable
  className="-striped -highlight"
  columns=columns
  data=coursesData
  defaultPage=currentPage
  defaultPageSize=courses.perPage
  loading=isLoading
  manual=true
  onFetchData=setFilter
/>

设置过滤功能:

const setFilter = (pagination: any) => 
  props.dispatch(updateCoursePageSize(pagination.pageSize));
  props.dispatch(updateCourseCurrentPage(pagination.page + 1));
  setCurrentPage(pagination.page);
  setPerPage(pagination.pageSize);
  setLoading(true);
  props.dispatch(fetchCourses()).then(() => 
    setLoading(false);
  );
;

有谁知道如何清理 react 中的钩子

【问题讨论】:

【参考方案1】:

使用 useEffect 您可以返回一个将在清理时运行的函数。所以在你的情况下,你会想要这样的东西:

useEffect(() => 
  let unmounted = false;

  setPageLoading(true);

  props
    .dispatch(fetchCourses())
    .then(() => 
      if (!unmounted) 
        setPageLoading(false);
      
    )
    .catch((error: string) => 
      if (!unmounted) 
        toast.error(error);
        setPageLoading(false);
      
    );

  return () =>  unmounted = true ;
, []);

编辑:如果您需要在 useEffect 之外启动一个调用,那么它仍然需要检查一个未安装的变量以判断它是否应该跳过对 setState 的调用。该未安装变量将由 useEffect 设置,但现在您需要克服一些障碍才能使该变量在效果之外可访问。

const Example = (props) => 
  const unmounted = useRef(false);
  useEffect(() => 
    return () =>  unmounted.current = true 
  , []);

  const setFilter = () => 
    // ...
    props.dispatch(fetchCourses()).then(() => 
      if (!unmounted.current) 
        setLoading(false);
      
    )
  

  // ...
  return (
    <ReactTable onFetchData=setFilter /* other props omitted */ />
  );

【讨论】:

但是如果在没有 useEffect 的情况下再次执行相同的 API 调用会怎样。例如,如果 API 调用完成是 useEffect 并在函数中再次执行。 @Nicholas Tower 如果您看到 setFilter 函数,则会调用相同的调度调用 fetchCourses()。如果两个调用同时发生,我会收到内存泄漏警告。如果我在 setFilter 函数中隐藏调用,则没有内存泄漏警告【参考方案2】:

其他答案当然有效,我只是想分享一个我想出的解决方案。 我构建了这个hook,它的工作原理与 React 的 useState 类似,但只有在安装组件时才会设置状态。我发现它更优雅,因为您不必在组件中使用 isMounted 变量!

安装:

npm install use-state-if-mounted

用法:

const [count, setCount] = useStateIfMounted(0);

您可以在钩子的npm page 上找到更多高级文档。

【讨论】:

这个 repo 的作者说这不是一个可行的解决方案(只是隐藏了警告信息)。我也很沮丧,但仅供大家参考。【参考方案3】:

内存泄漏发生,当一个不必要的并且应该从内存中清除的东西因为其他东西仍然持有它而被保留时。在 React 组件的情况下,组件中进行的异步调用可能会保存 setState 或其他引用的引用,并将保存它们直到调用完成。 您看到的警告来自 React,它说某些东西仍在保持和设置组件实例的状态,该组件实例在组件卸载时很久以前就从树中删除了。现在使用标志来不设置状态只会删除警告但不会删除内存泄漏,即使使用 Abort 控制器也是如此。为了避免这种情况,您可以使用状态管理工具来帮助调度一个操作,该操作将在组件外部进行处理,而无需保存组件的任何内存引用,例如 redux。如果您不使用此类工具,那么您应该找到一种方法来清除组件卸载时传递给异步调用的回调(然后,catch,finally 块)。在下面的 sn-p 中,我正在做同样的事情,分离对传递给异步调用的方法的引用以避免内存泄漏。 这里的 Event Emitter 是一个 Observer,你可以创建一个或者使用一些包。

const PromiseObserver = new EventEmitter();

class AsyncAbort 
  constructor() 
    this.id = `async_$getRandomString(10)`;
    this.asyncFun = null;
    this.asyncFunParams = [];
    this.thenBlock = null;
    this.catchBlock = null;
    this.finallyBlock = null;
  

  addCall(asyncFun, params) 
    this.asyncFun = asyncFun;
    this.asyncFunParams = params;
    return this;
  

  addThen(callback) 
    this.thenBlock = callback;
    return this;
  

  addCatch(callback) 
    this.catchBlock = callback;
    return this;
  

  addFinally(callback) 
    this.finallyBlock = callback;
    return this;
  

  call() 
    const callback = ( type, value ) => 
      switch (type) 
        case "then":
          if (this.thenBlock) this.thenBlock(value);
          break;
        case "catch":
          if (this.catchBlock) this.catchBlock(value);
          break;
        case "finally":
          if (this.finallyBlock) this.finallyBlock(value);
          break;
        default:
      
    ;
    PromiseObserver.addListener(this.id, callback);
    const cancel = () => 
      PromiseObserver.removeAllListeners(this.id);
    ;
    this.asyncFun(...this.asyncFunParams)
      .then((resp) => 
        PromiseObserver.emit(this.id,  type: "then", value: resp );
      )
      .catch((error) => 
        PromiseObserver.emit(this.id,  type: "catch", value: error );
      )
      .finally(() => 
        PromiseObserver.emit(this.id,  type: "finally" );
        PromiseObserver.removeAllListeners(this.id);
      );
    return cancel;
  


在 useEffect 钩子里你可以做

React.useEffect(() => 
    const abort = new AsyncAbort()
      .addCall(simulateSlowNetworkRequest, [])
      .addThen((resp) => 
        setText("done!");
      )
      .addCatch((error) => 
        console.log(error);
      )
      .call();
    return () => 
      abort();
    ;
  , [setText]);

我从here 分叉了某人的代码以使用上述逻辑,您可以在下面的链接中查看它的实际操作 link

【讨论】:

以上是关于如何在useEffect钩子反应中停止内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

如何避免本机反应中的内存泄漏?

反应钩子。无法对未安装的组件执行 React 状态更新

停止重新渲染反应功能组件(使用钩子)

如何在useeffect钩子中停止无限循环

如何将 React 钩子(useContext、useEffect)与 Apollo 反应钩子(useQuery)结合起来

当我的 useEffect 钩子在 react-apollo 突变后被触发时,如何从反应中解决这个警告?