前言
在使用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.
这种场景一般是在某个组件如 Modal中发送请求,但是这个组件突然被销毁了。而组件里面的请求还在继续发送,等请求回来之后会调用setState去更新数据, 但是此时组件已经销毁了。所以会给出一个警告。
场景复现
通常我们都是直接使用 useState,这种用法在执行同步代码时没什么问题,但是如果是在异步回调函数中执行可能会由于组件先被销毁导致代码报错。测试代码如下:
import { useState, useEffect } from 'react';
function Modal() {
const [state, setState] = useState(0);
useEffect(() => {
setTimeout(() => {
setState((s) => s + 1);
}, 5000);
}, []);
}
考虑如上代码,如果组件在 5 秒内被销毁,setState((s) => s + 1)依然会执行,执行后将会报错: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.
原因分析
我们是在销毁的组件中去调用setState导致的这个问题,那么在销毁的组件中不调用setState不就可以了。因此我们可以添加一个开关来控制是否调用setState
useSafeState
官方的 useState 的 api 为[state, setState] = useState(initialState), 那我们需要与官方方式的保持一致,让使用者无差别感知。
import React, {
useRef,
useState,
useEffect,
useCallback,
Dispatch,
SetStateAction,
} from 'react';
/**
* 更安全的 useState 函数
* 在有些时候我们会在请求接口之后执行 setState 操作
* 但此时组件有可能已经被销毁导致控制台出现报错
* 所以我们提供一个更加安全的接口来实现这个行为
*
* @param initialState
* @returns
*/
export function useSafeState<S>(initialState: S | (() => S)) {
const mountRef = useRef(false);
useEffect(() => {
mountRef.current = true;
return () => {
mountRef.current = false;
};
}, []);
const [state, setState] = useState(initialState);
const setSafeState = useCallback((newState: S) => {
if (mountRef.current) {
setState(newState);
}
}, []);
return [state, setSafeState] as [S, Dispatch<SetStateAction<S>>];
}
结语
这样我们自定义一个hooks解决了setState控制台报错的问题, 社区有很多的优秀的 React hooks 和 Vue hooks 一般可以搜索关键字获得