React 组件在卸载后,如果组件内有正在处理的异步函数。那么在函数处理后,需要更新组件的状态,会得到一个警告。

❗️ 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.

出现的原因

最可能出现在以下几个情况:

  • 在组件内调用了网络请求,网络请求未响应就卸载了组件
  • 在组件内调用了定时器,定时器还没执行就卸载了组件

网络请求

最常见的情况就是在网络请求中,当我们发起的网络请求还未返回,我们切换了组件,导致之前的组件被卸载掉。

// Posts.tsx
import { useEffect, useState } from 'react';
type Post = { id: number | string; title: string };

const Posts = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await response.json();
        setPosts(data);
      } catch (e) {
        console.log(e);
      }
    };

    fetchData();
  }, []);
  return (
    <ul>
      {posts.map((post) => {
        return <li key={post.id}>{post.title}</li>;
      })}
    </ul>
  );
};

export default Posts;
// App.tsx
import { useState } from 'react';
import Posts from './Posts';

export default function App() {
  const [showPost, setShowPost] = useState(false);
  return (
    <div>
      <button onClick={() => setShowPost(true)}>Show Post</button>
      <button onClick={() => setShowPost(false)}>Hide Post</button>
      {showPost && <Posts />}
    </div>
  );
}

https://codesandbox.io/s/fetch-call-1xt2tb

在上面的 Posts 组件挂载后,会发起向 JSON Placeholder 的 API 请求,并在请求成功后展示博客列表。

当我们点击 ‘Fetch Posts’ 按钮后,并且迅速点击 ‘Hide Posts’(也可以通过开发者工具,将 ‘Network’ 调到慢速,防止网速过快)。在网络请求成功返回之前,我们切换组件的状态,就会得到相应的错误。

定时器

另一种很常见的情况就是 setTimeout 和 setInterval 定时器的使用。

// Timeout.tsx
import { useState } from 'react';

const Timeout = () => {
  const [counter, setCounter] = useState(0);
  setTimeout(() => setCounter(counter + 1), 5000);
  return (
    <>
      <div>Counter: {counter}</div>
      <div>The Timeout will add after 5 seconds.</div>
    </>
  );
};

export default Timeout;

https://codesandbox.io/s/settimeout-0c5ro1

当 Timeout 组件挂载后,会设置 5s 后更新 counter 变量,期间我们卸载掉组件,就会得到相应的错误。

解决方案

以上就是常见的两种遇到该警告提示的情况。解决方案有以下方案可以参考。

更新前检测组件状态

使用 useRef 关联组件的生命周期,当组件挂载时设置变量为 true,当卸载之后设置变量为 false。在异步函数更新状态的时候,先判断 isMounted.current 的变量值。

const isMounted = useRef();

useEffect(() => {
  isMounted.current = true;
  return () => {
    isMounted.current = false;
  };
}, []);

if (isMounted.current) {
  setState(..);
}

https://codesandbox.io/s/settimeout-useref-zjnr7c

可以将上面的 useRef 代码封装成自定义的 hook,也可以直接使用第三方封装的,如 react-use 的 useAsync

import { useAsync } from 'react-use';
type Post = { id: number | string; title: string };

const Posts = () => {
  const state = useAsync(async () => {
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await response.json();
      return data;
    } catch (e) {
      console.log(e);
    }
  }, []);

  return (
    <ul>
      {state.value &&
        state.value.map((post: Post) => {
          return <li key={post.id}>{post.title}</li>;
        })}
    </ul>
  );
};

export default Posts;

https://codesandbox.io/s/fetch-call-useasync-evtz0n

卸载时处理异步回调

可以在卸载组件时,处理掉异步调用函数的组件。

对于接口的调用,我们可以通过调用 AbortController 的接口,取消网络请求。

// Posts.tsx
import { useEffect, useState } from 'react';
type Post = { id: number | string; title: string };

const Posts = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
          signal: signal,
        });
        const data = await response.json();
        setPosts(data);
      } catch (e) {
        console.log(e);
      }
    };

    fetchData();
    return () => {
      controller.abort();
    };
  }, []);
  return (
    <ul>
      {posts.map((post) => {
        return <li key={post.id}>{post.title}</li>;
      })}
    </ul>
  );
};

export default Posts;

https://codesandbox.io/s/fetch-call-abortcontroller-x0c5ro

对于 fetch, axios 均可以使用 AbortController 来进行管理卸载,当请求时,附带参数 AbortSignal,卸载组件时,调用 abort() 方法。

对于定时器的取消,则是调用 clearTimeoutclearInterval 来取消定时器。

import { useEffect, useState } from 'react';

const Timeout = () => {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    const timerID = setTimeout(() => setCounter((counter) => counter + 1), 5000);
    return () => clearTimeout(timerID);
  }, []);
  return (
    <>
      <div>Counter: {counter}</div>
      <div>The Counter will add after 5 seconds.</div>
    </>
  );
};

export default Timeout;

https://codesandbox.io/s/settimeout-cleartimeout-yhup3u

最后

以上就是常见的警告提示以及对应的解决方案。

但是,最后,在 React 18 版本中,取消了这个警告提示!!!

https://codesandbox.io/s/settimeout-react-18-w22v5u

具体取消原因可以查看 Update to remove the “setState on unmounted component” warning #82,大致意思大多数情况并不会真正造成内存泄漏。

参考链接