每个 React 开发者都讨厌的熟悉 Bug
\n您是否曾在浏览器控制台中看到过这样的警告 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.?如果是——您并不孤单。这个问题在 Stack Overflow 上已经获得了 数千次浏览,并成为 React 新手和经验丰富的开发者的真正痛点。
在本文中,我们将分析为什么会出现这个错误,如何使用 TypeScript 修复它,最重要的是——如何优化您应用程序的性能,使此类警告不再出现。开始吧!
\n\n为什么 React 会报错 unmounted component?
\n想象一下:用户打开了一个页面,开始加载数据(例如通过 fetch 或 axios),但没有等待响应就切换到了另一个屏幕。发起请求的组件已经被卸载(从 DOM 中移除),但异步操作仍在执行。当响应到达时,代码试图更新一个已经不存在的组件的状态(state)。React 理所当然地抗议:“嘿,我无法更新不存在的东西!”。
在 TypeScript 中,如果您使用严格的类型检查并且忘记了 useEffect 中的 cleanup 函数,这个问题尤其明显。
问题的经典示例 (TypeScript)
\nimport React, { useState, useEffect } from 'react';\n\ninterface User {\n id: number;\n name: string;\n}\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n\n useEffect(() => {\n fetch(`/api/users/${userId}`)\n .then(res => res.json())\n .then(data => setUser(data)); // 错误!组件可能已被卸载\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\n如果用户快速切换路由,UserProfile 组件将被移除,而 setUser 会在空组件上被调用。结果导致内存泄漏和控制台错误。
修复错误的 3 种方法(附 TypeScript 示例)
\n让我们看看三种可靠的方法,它们可以解决这个问题并提高React 应用程序的性能。
\n\n方法 1:使用 useRef 的标志(最流行)
\n创建一个 ref 标志,指示组件是否已挂载。仅当标志为 true 时才更新状态。
import React, { useState, useEffect, useRef } from 'react';\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n const isMounted = useRef(true);\n\n useEffect(() => {\n // 每次挂载时重置标志\n isMounted.current = true;\n\n fetch(`/api/users/${userId}`)\n .then(res => res.json())\n .then(data => {\n if (isMounted.current) {\n setUser(data);\n }\n });\n\n // 清理:卸载时将标志设置为 false\n return () => {\n isMounted.current = false;\n };\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\n优点:简单,不需要额外的库。缺点:需要记住进行清理。
\n\n方法 2:使用 AbortController 取消 fetch 请求(推荐)
\n现代且正确的方法是取消请求本身,而不仅仅是忽略响应。这可以节省服务器和客户端资源。
\nimport React, { useState, useEffect } from 'react';\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const controller = new AbortController();\n const signal = con