如何在 useEffect 钩子react中阻止内存泄漏

IT技术 reactjs react-table use-effect
2021-05-06 03:06:23

我正在使用 Effect 钩子从服务器获取数据,这些数据被传递到react表,在那里我使用相同的 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);
  });
}, []);

react表页面:

<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中的钩子

3个回答

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

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

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

我从这里分叉了某人的代码以使用上述逻辑,您可以在下面的链接链接中检查它的运行情况

其他答案当然有效,我只是想分享我想出的解决方案。我构建了这个钩子,它的工作原理与 React 的 useState 一样,但只有在组件被挂载时才会 setState。我发现它更优雅,因为您不必在组件中使用 isMounted 变量!

安装 :

npm install use-state-if-mounted

用法 :

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

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