React-hooks。无法对未装载的组件执行 React 状态更新

我得到了这个错误:

无法对已卸载的组件执行 React 状态更新 但它表明应用程序中存在内存泄漏。要修复, 取消 useEffect 清理中的所有订阅和异步任务 功能。

当开始取数据和卸载组件时,函数尝试更新卸载组件的状态。

解决这个问题的最好方法是什么?

CodePen 示例

default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)


useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)


return () => clearInterval(timer)
}, [])


async function updateNotSeenAmount() {
let data // here i fetch data


setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}


async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}


return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
127340 次浏览

最简单的解决方案是使用一个局部变量来跟踪组件是否安装。这是基于类的方法的常见模式。下面是用钩子实现它的 一个例子:

function Example() {
const [text, setText] = React.useState("waiting...");


React.useEffect(() => {
let isCancelled = false;


simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});


return () => {
isCancelled = true;
};
}, []);


return <h2>{text}</h2>;
}

这里是 另一个选择useRef(见下文)。请注意,如果有一个依赖项列表,这个解决方案将无法工作。在第一次渲染之后,ref 的值将保持为真。在这种情况下,第一种解决方案更为合适。

function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");


React.useEffect(() => {
fetch();


return () => {
isCancelled.current = true;
};
}, []);


function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}


return <h2>{text}</h2>;
}

您可以在这个 文章中找到关于这个模式的更多信息。下面是 GitHub 上 React 项目中的 一个问题,它展示了这个解决方案。

如果您正在从 axios (使用 hook)获取数据,并且仍然发生错误,那么只需将 setter 包装在条件中

let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);

DR

下面是一个 CodeSandBox的例子

其他的答案当然可以,我只是想和大家分享一个我想到的解决方案。 我构建了这个 钩子,它的工作方式与 React 的 useState 类似,但是只有在组件挂载的情况下才会使用 setState。我发现它更优雅,因为您不必在组件中使用 isMounted 变量!

安装:

npm install use-state-if-mounted

用法:

const [count, setCount] = useStateIfMounted(0);

您可以在钩子的 Npm 页面中找到更高级的文档。

这里有一个简单的解决方案。这个警告是由于当请求在后台执行一些获取请求时(因为有些请求需要一些时间)我们从屏幕导航回来,然后反应不能更新状态。这里是这个的示例代码。在每个状态更新之前写下这一行。

if(!isScreenMounted.current) return;

下面是完整的例子

import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () =>  isScreenMounted.current = false
},[])


const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
 

var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
         

formdata.append("file",file);
         

fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}


return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}


export default SearchScreen;




const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})

这个答案与具体的问题无关,但我得到了相同的 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.和作为一个 对新人做出反应无法找到一个解决方案。

我的问题与一个未安装的组件中的 useState有关。

我注意到我在卸载组件的函数之后调用 set state 函数(setIsLoading) :

const Login = () => {
const [isLoading, setIsLoading] = useState(false);


const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}

正确的方法是在组件仍然挂载时调用 setIsLoading,然后在特定情况下调用函数来卸载/处理用户登录:

    firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)

您可以将与状态相关的数据添加到 useEffect 主体中,以避免在每个重新呈现进程中都重新运行这些数据。这种方法可以解决这个问题。

useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)


return () => clearInterval(timer)
}, [notSeenAmount])

REF: < a href = “ https://reactjs.org/docs/hooks-effect.html # Tip-Optiming-Performance-by-Skipping-effect”rel = “ nofollow noReferrer”> Tip: 籍跳过效果优化性能

自定义 Hook 解决方案(ReactJs/NextJs)

创建一个名为“ share”的新文件夹,并在其中添加两个名为“ hooks”和“ utils”的文件夹。在 utils 文件夹中添加一个名为‘ commonFunctions.js’的新文件,并添加下面的代码片段。

export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};

在 Hook 文件夹中添加一个名为“ get-hook. js”的新文件,并添加下面的代码片段。

import { useCallback, useEffect, useRef } from "react";


import { promisify } from "../utils/commonFunctions";


export const useFetch = () => {
const isUnmounted = useRef(false);


useEffect(() => {
isUnmounted.current = false;


return () => {
isUnmounted.current = true;
};
}, []);


const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);


if (!isUnmounted.current) {
console.log('updating state..');


onSuccess(response.data);
}
else
console.log('aborted state update!');


console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);


if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);


return { call }
};

文件夹结构

enter image description here

我们的自定义钩子现在已经准备好了。我们在组件中使用它,如下所示

const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();


// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}

在我们的组件中,我们调用“ updateSubtions”方法,它将触发我们使用自定义钩子的“ getSubtions”方法

enter image description here

想看看对面吗?

用下面的方法更改“ getSubscrids”方法

const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);


setSubscriptions(response);
}, [userId]);

现在尝试调用“ updateSubtions”方法,并在5秒钟之前导航到另一个页面

enter image description here

试试这个定制的钩子:

import { useEffect, useRef } from 'react';


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

function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);


useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);


return <h2>{text}</h2>;
}