在卸载组件上使用 React SetState 避免内存泄漏

2025-06-10

在卸载组件上使用 React SetState 避免内存泄漏

如果你在 React 应用程序中看到此错误,请举手✋:

Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
Enter fullscreen mode Exit fullscreen mode

问题

此错误通常发生在你发出异步数据请求但组件卸载时。例如,应用中的某些逻辑告诉 React 离开该组件。

您仍然有一个待处理的远程数据请求,但是当数据到达并修改组件的状态时,应用程序已经呈现了不同的组件。

来自React 博客

“setState 警告” 的存在是为了帮助你捕获 bug,因为调用setState()一个已卸载的组件可能表明你的应用/组件未能正确清理。具体来说,调用setState()一个已卸载的组件意味着你的应用在组件卸载后仍然持有对该组件的引用——这通常意味着存在内存泄漏!

在这篇文章中,我将展示一些避免数据获取时发生内存泄漏的可能的解决方法。

为什么会发生这种情况?

获取数据时,会发出异步请求。通常,你会使用基于 Promised 的 API 来实现,例如浏览器原生的 API fetch

fetch示例:使用(基于 Promise)调用 API

function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    const fetchData = () => {
      setState(prevState => ({ ...prevState, isLoading: true }))

      fetch('https://ghibliapi.herokuapp.com/people')
        .then(response => response.json())
        .then(jsonResponse => {
          setState(prevState => {
            return {
              ...prevState,
              isLoading: false,
              loadedData: [...jsonResponse],
            }
          })
        })
        .catch(_err => {
          setState(prevState => {
            return { ...prevState, isLoading: false, isError: true }
          })
        })
    }

    // calling the function starts the process of sending ahd
    // storing the data fetching request
    fetchData()
  }, [])

  return <JSX here />
}
Enter fullscreen mode Exit fullscreen mode

你可以使用async/await重写数据获取方法,但这本质上仍然是 JavaScript 的 Promise。JavaScript

单线程的,所以在执行异步操作时,你无法避免“暂停”代码。这就是为什么你需要事件监听器、回调、promise或 async/await。

问题是您无法取消 Promise。

现在,你的应用可能会更改视图,但承诺尚未兑现。你无法在启动数据获取过程后中止它。

因此,就会发生上述错误。

互联网搜索提供的典型解决方案

  1. 使用第三方库,例如bluebirdaxios

    问题:项目中的另一个依赖项(但 API 通常比自己动手更容易)

  2. 使用可观察对象

    问题:你现在又引入了另一个复杂程度

  3. 使用以下方式跟踪组件的状态isMounted

    问题:这是一种反模式

  4. 创建您自己的取消方法

    问题:它引入了另一个围绕 Promises 的包装器

  5. 使用XMLHttpRequest

    问题:代码比 稍微冗长一些fetch,但您可以轻松取消网络请求

让我们看看一些建议:

跟踪安装状态

以下解决方法得到了Robin WieruchDan Abramov等知名 React 作者的推荐。

这些开发者在 React 方面肯定比我聪明得多。

他们将该解决方案描述为权宜之计。它并不完美。

function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    // we have to keep track if our component is mounted
    let isMounted = true

    const fetchData = () => {
      // set the state to "Loading" when we start the process
      setState(prevState => ({ ...prevState, isLoading: true }))

      // native browser-based Fetch API
      // fetch is promised-based
      fetch('https://ghibliapi.herokuapp.com/people')
        // we have to parse the response
        .then(response => response.json())
        // then we have to make sure that we only manipulate
        // the state if the component is mounted
        .then(jsonResponse => {
          if (isMounted) {
            setState(prevState => {
              return {
                ...prevState,
                isLoading: false,
                loadedData: [...jsonResponse],
              }
            })
          }
        })
        // catch takes care of the error state
        // but it only changes statte, if the component
        // is mounted
        .catch(_err => {
          if (isMounted) {
            setState(prevState => {
              return { ...prevState, isLoading: false, isError: true }
            })
          }
        })
    }

    // calling the function starts the process of sending ahd
    // storing the data fetching request
    fetchData()

    // the cleanup function toggles the variable where we keep track
    // if the component is mounted
    // note that this doesn't cancel the fetch request
    // it only hinders the app from setting state (see above)
    return () => {
      isMounted = false
    }
  }, [])

  return <JSX here />
}
Enter fullscreen mode Exit fullscreen mode

(如果您有兴趣,这里有一个CodeSandBox 链接。)

严格来说,你不会取消数据获取请求。解决方法是检查组件是否已挂载。setState如果组件未挂载,则避免调用此方法。

但网络请求仍然处于活动状态。

创建您自己的取消方法

上述博客文章介绍了 Promise 的包装器:

const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise
Enter fullscreen mode Exit fullscreen mode
const makeCancelable = promise => {
  let hasCanceled_ = false

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
      error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
    )
  })

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

或者,你可以在XMLHttpRequest周围引入一个取消方法。Axios

使用类似的方法,即取消令牌

以下是来自StackOverflow的代码

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

// now you can setup the cancellation
var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();
Enter fullscreen mode Exit fullscreen mode

这是一个CodeSandBox 示例

两种解决方案都引入了一个新的辅助函数。第二个函数已经将我们引向了XMLHttpRequest的方向

XMLHttpRequest 的低级 API

StackOverflow 代码将您的 API 调用包装到XMLHttpRequest 的Promise 中。它还添加了一个取消令牌。

为什么不使用 XMLHttpRequest 本身?

当然,它不像浏览器原生那样可读fetch。但我们已经确定必须添加额外的代码来取消承诺。

XMLHttpRequest 允许我们在不使用 Promise 的情况下中止请求。这是一个使用 的简单实现useEffect

useEffect函数使用 来清理请求abort

function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    // we have to create an XMLHTTpRequest opject
    let request = new XMLHttpRequest()
    // we define the responseType
    // that makes it easier to parse the response later
    request.responseType = 'json'

    const fetchData = () => {
      // start the data fetching, set state to "Loading"
      setState(prevState => ({ ...prevState, isLoading: true }))

      // we register an event listener, which will fire off
      // when the data transfer is complete
      // we store the JSON response in our state
      request.addEventListener('load', () => {
        setState(prevState => ({
          ...prevState,
          isLoading: false,
          loadedData: [...request.response],
        }))
      })

      // we register an event listener if our request fails
      request.addEventListener('error', () => {
        setState(prevState => ({
          ...prevState,
          isLoading: false,
          isError: true,
        }))
      })

      // we set the request method, the url for the request
      request.open('GET', 'https://ghibliapi.herokuapp.com/people')
      // and send it off to the aether
      request.send()
    }

    // calling the fetchData function will start the data fetching process
    fetchData()

    // if the component is not mounted, we can cancel the request
    // in the cleanup function
    return () => {
      request.abort()
    }
  }, [])

  return <JSX here />
}
Enter fullscreen mode Exit fullscreen mode

您可以在CodeSandBox上看到它的实际运行。

这还不错,而且你避免了恼人的 React 警告。

由于 XMLHttpRequest API 不太直观,这段代码比较难理解。除此之外,它只比基于 promise 的版本多了几行代码fetch——而且支持取消功能!

结论

现在我们已经看到了一些避免在未安装的组件上设置状态的方法。

最好的方法是排查代码问题。或许你可以避免卸载组件。

但如果你需要其他方法,你现在已经了解了一些避免在获取数据时出现 React 警告的方法。

致谢

使用 XMLHttpRequest 的想法并非我所想。Cheng

Lou在ReasonML Discord 频道向我介绍了它,甚至还给出了一个 ReasonReact 中的示例。

链接

鏂囩珷鏉ユ簮锛�https://dev.to/sophiabrandt/avoid-memory-leak-with-react-setstate-on-an-unmounted-component-4650
PREV
如何摆脱困境
NEXT
如何用 Javascript 创建一个简单的物理引擎 - 第一部分