如何在 useEffect 钩子中防止内存泄漏的 React

33

我正在使用Effect Hook从服务器获取数据,并将这些数据传递给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.

Effect Hook:

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中的hook吗?

4个回答

66

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

React 18已经移除了这个警告信息,而且不再需要通过一些变通方法来摆脱它。他们移除它的部分原因是它一直有点误导性。它说你有一个内存泄漏,但往往并非如此。

在问题中的代码 - 事实上,大多数引起这个警告的代码 - 在组件卸载后会运行一段有限的时间,然后设置状态,然后运行结束。由于运行结束,JavaScript可以释放闭包中的变量,因此通常不会出现泄漏。

如果您设置了一个持久性订阅,它会无限期地继续下去,那么您将会有一个内存泄漏的情况。例如,也许您设置了一个WebSocket并监听消息,但您从未关闭该WebSocket。这些情况需要修复(通过向useEffect提供一个清理函数),但它们并不常见。

React 18移除警告的另一个原因是他们正在致力于实现组件在卸载后保留其状态的能力。一旦这个功能在React中实现,卸载后设置状态将成为一种完全有效的操作。

原始答案(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设置,但现在你需要经过一些困难来使变量在effect之外可访问。
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中完成,然后在一个函数中再次调用。 - Nidhin Kumar
@Nicholas Tower,如果您看到setFilter函数,相同的调度调用fetchCourses()。 如果两个调用同时进行,我会收到内存泄漏警告。 如果我将setFilter函数中的调用隐藏起来,则不会收到内存泄漏警告。 - Nidhin Kumar
@NicholasTower 在你的第一个例子中,为什么在 useEffect() 中使用 let unmounted = false 可以工作?每次运行 useEffect() 时,它都会将 unmounted 重置为 false,因此清除函数没有任何效果? - Shuzheng
useEffect 的依赖数组为空,因此它只会运行一次。即使它运行多次,它仍然有用,只是名称不准确(因为它未必已经卸载)。每次 effect 运行时都会创建一个全新的本地变量 unmounted。新变量可能是 false,但旧变量是 true。来自上一个 effect 的代码关闭了旧变量,因此旧代码可以检查它以确定它是过时 effect 的一部分并且什么也不做。 - Nicholas Tower

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组件中,组件中进行的异步调用可能会持有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]);

我从这里fork了别人的代码,用于上述逻辑,你可以在下面的链接中查看它的实际运行情况link


-1
其他答案当然可行,我只是想分享我想出的一个解决方案。 我构建了这个 hook,它的工作方式与 React 的 useState 类似,但只会在组件挂载时进行 setState。我觉得更优雅,因为你不必在组件中搞一个 isMounted 变量!

安装:

npm install use-state-if-mounted

使用方法:

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

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


17
这个存储库的作者说这不是一个可行的解决方案(只是隐藏了警告信息)。我也感到失望,但提醒大家知道。 - kennysong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接