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

How to stop memory leak in useEffect hook react

我正在使用 Effect hook 从服务器获取数据并将这些数据传递给反应 table 在那里我使用相同的 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 Table 页:

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

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

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

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

安装:

npm install use-state-if-mounted

用法:

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

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

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

您可以像这样创建一个自定义挂钩:

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