# React在未挂载组件后更新状态的错误

6 min read
Table of Contents

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() 方法。

对于定时器的取消,则是调用 clearTimeout 和 clearInterval 来取消定时器。

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,大致意思大多数情况并不会真正造成内存泄漏。

参考链接


More Posts

# 生成器 Generator

4 min read

生成器 Generator 使得函数可以通过 yield 暂停,并且暂停还可以通信,通信结束后可以继续执行。其上下文会被保存。