如何阻止useEffect hook React中的内存泄漏

问题描述 投票:0回答:4

我正在使用 Effect hook 从服务器获取数据,这些数据被传递到反应表,我使用相同的 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中的hook

reactjs react-table use-effect
4个回答
66
投票

2022 年 6 月更新(2023 年底仍然准确):

React 18 已删除此警告消息,并且可能不再需要消除它的解决方法。他们删除它的部分原因是它总是有点误导。它说你有内存泄漏,但很多时候你并没有。

问题中的代码(实际上是导致此警告的大多数代码)在组件卸载后运行有限的时间,然后设置状态,然后完成运行。由于运行完毕,javascript 可以释放其闭包中的变量,因此通常不会发生泄漏。

如果您正在设置无限期持续的持久订阅,则会出现内存泄漏。例如,也许您设置了一个 websocket 并监听消息,但您从未拆除该 websocket。这些情况确实需要修复(通过向

useEffect
提供清理功能),但它们并不常见。

React 18 删除警告的另一个原因是他们正在研究组件在卸载后保留其状态的能力。一旦该功能生效,卸载后设置状态将是完全有效的事情。

原始答案(2019年9月):

使用 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 */ />
  );
}

2
投票

您可以为此创建一个自定义挂钩:

import * as React from 'react';

export default function useStateWhenMounted<T>(initialValue: T) {
  const [state, setState] = React.useState(initialValue);
  const isMounted = React.useRef(true);
  React.useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  const setNewState = React.useCallback((value) => {
    if (isMounted.current) {
      setState(value);
    }
  }, []);

  return [state, setNewState];
}


1
投票

当一个不必要的、应该从内存中清除的东西因为其他东西仍然保留而被保留时,就会发生内存泄漏。在 React Component 的情况下,组件中进行的异步调用可能会保留 setState 的引用或其他引用,并将保留它们直到调用完成。 您看到的警告来自 React,表示某些组件仍在保留并设置组件实例的状态,该组件实例早在组件卸载时就已从树中删除。现在,使用标志不设置状态只会删除警告,但不会删除内存泄漏,即使使用 Abort 控制器也会执行相同的操作。为了避免这种情况,您可以使用状态管理工具来帮助分派一个操作,该操作将在组件外部进行处理,而不持有组件的任何内存引用,例如 redux。如果您没有使用此类工具,那么您应该找到一种方法来清除组件卸载时传递给异步调用的回调(然后,catch,finally 块)。在下面的代码片段中,我执行相同的操作,分离对传递给异步调用的方法的引用,以避免内存泄漏。 这里的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分叉某人的代码以使用上述逻辑,您可以在下面的链接中检查它的实际情况 链接


-1
投票

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

安装:

npm install use-state-if-mounted

用途:

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

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

© www.soinside.com 2019 - 2024. All rights reserved.