React删除元素触发“无法在已卸载组件更新状态”警告的排查与解决
问题描述
我有一个React页面,用Card组件展示数据列表。页面加载时通过useQuery从GraphQL(Apollo Client)获取数据,支持Card的编辑、删除、新建操作,并用本地state存储获取到的结果。执行删除操作的mutation后,列表能正常重新渲染,但会出现React警告:Can't perform a React state update on an unmounted component。
相关核心代码如下:
初始化与state部分
const {data: indicatorsData} = queryIndicators() const {createQueryIndicator} = createQueryIndicatorMutation() const {deleteQueryIndicator} = deleteQueryIndicatorMutation() const [isOpened, setIsOpened] = useState(false) const [form] = useForm() const [displayedQueryIndicators, setDisplayedQueryIndicators] = useState<QueryIndicator[]>([]) useEffect(() => { if (indicatorsData) { setDisplayedQueryIndicators(() => indicatorsData.queryIndicators) } }, [indicatorsData])
删除操作逻辑
const handleCancel = () => { setIsOpened(false) } const handleDeleteIndicator = (uuid: any) => { deleteQueryIndicator({variables: {uuid}}) .then(result => { const newList = displayedQueryIndicators.filter( indicator => indicator.uuid !== result.data.deleteQueryIndicator) setDisplayedQueryIndicators(newList) }) .catch((info) => {console.log(info)}) }
页面渲染部分(删除按钮)
<Row gutter={12}> <Col span={8}> <EditOutlined onClick={() => console.log("")} key="edit"/> </Col> <Col span={8}> <RedoOutlined onClick={() => console.log("")} key="refresh"/> </Col> <Col span={8}> <Popconfirm placement="top" title={'Are you sure ?'} onConfirm={() => handleDeleteIndicator(indicator.uuid)} okText="Yes" cancelText="No" > <DeleteOutlined onClick={() => console.log("")} key="delete"/> </Popconfirm> </Col> </Row>
问题原因
这个警告的本质是:异步操作(这里是deleteQueryIndicator的mutation请求)完成后,回调函数里尝试更新一个已经被卸载的组件的state。
具体来说,当用户触发删除操作后,如果在mutation请求还没完成的情况下,组件就被卸载了(比如用户跳转到其他页面),此时then回调里的setDisplayedQueryIndicators依然会执行,试图更新已不存在的组件的状态,React就会抛出这个警告。
另外,当前手动维护displayedQueryIndicators本地state的写法,还可能带来竞态问题(比如多个异步操作顺序执行导致state更新错误)。
解决方案
方案1:使用Apollo Client缓存自动更新(推荐)
Apollo Client本身具备缓存管理能力,完全不需要手动维护本地state来同步列表数据。直接利用Apollo的缓存更新机制,让useQuery自动响应缓存变化重新渲染列表,从根源上避免手动setState带来的问题。
步骤:
- 移除本地
displayedQueryIndicators状态和对应的useEffect同步逻辑,直接用useQuery返回的indicatorsData.queryIndicators渲染列表。 - 在执行
deleteQueryIndicator时,通过update函数手动更新Apollo缓存:
// 假设queryIndicators对应的GraphQL文档变量为queryIndicatorsDocument const handleDeleteIndicator = (uuid: any) => { deleteQueryIndicator({ variables: { uuid }, update(cache) { // 读取缓存中的当前列表数据 const cachedData = cache.readQuery({ query: queryIndicatorsDocument }); if (!cachedData) return; // 过滤掉被删除的项,更新缓存 const updatedIndicators = cachedData.queryIndicators.filter( indicator => indicator.uuid !== uuid ); cache.writeQuery({ query: queryIndicatorsDocument, data: { queryIndicators: updatedIndicators } }); } }) .catch(info => console.log(info)); }
这样Apollo会自动更新缓存,useQuery会监听缓存变化并重新渲染组件,完全不需要手动处理state更新,自然也不会出现卸载后的state更新警告。
方案2:用AbortController取消异步请求
在组件卸载时取消未完成的mutation请求,避免回调函数执行。
import { useEffect, useRef } from 'react'; // 在组件内定义AbortController const abortControllerRef = useRef(new AbortController()); useEffect(() => { const controller = abortControllerRef.current; // 组件卸载时取消请求 return () => controller.abort(); }, []); const handleDeleteIndicator = (uuid: any) => { deleteQueryIndicator({ variables: { uuid }, // 将signal传递给请求 context: { fetchOptions: { signal: abortControllerRef.current.signal } } }) .then(result => { const newList = displayedQueryIndicators.filter( indicator => indicator.uuid !== result.data.deleteQueryIndicator ); setDisplayedQueryIndicators(newList); }) .catch(info => { // 忽略请求被取消的错误 if (info.name !== 'AbortError') { console.log(info); } }); }
方案3:用挂载状态标记控制state更新
维护一个标记组件是否挂载的ref,在异步回调执行前检查标记,仅当组件仍挂载时才更新state。
import { useEffect, useRef } from 'react'; const isMounted = useRef(true); useEffect(() => { // 组件卸载时将标记设为false return () => { isMounted.current = false; }; }, []); const handleDeleteIndicator = (uuid: any) => { deleteQueryIndicator({ variables: { uuid } }) .then(result => { // 检查组件是否仍挂载 if (!isMounted.current) return; const newList = displayedQueryIndicators.filter( indicator => indicator.uuid !== result.data.deleteQueryIndicator ); setDisplayedQueryIndicators(newList); }) .catch(info => console.log(info)); }
内容的提问来源于stack exchange,提问作者Anthony

