反应钩子。无法对未安装的组件执行 React 状态更新

Posted

技术标签:

【中文标题】反应钩子。无法对未安装的组件执行 React 状态更新【英文标题】:React-hooks. Can't perform a React state update on an unmounted component 【发布时间】:2019-10-19 21:46:16 【问题描述】:

我收到此错误:

无法对未安装的组件执行 React 状态更新。这是 无操作,但它表明您的应用程序中存在内存泄漏。修理, 在 useEffect 清理中取消所有订阅和异步任务 功能。

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

解决此问题的最佳方法是什么?

CodePen example。

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

【问题讨论】:

您的问题可能是您使用的 setInterval 不正确,请删除函数调用:即 updateNotSeenAmount 中的 updateNotSeenAmount 内的 ()setInterval @Khauri ye,已修复,但问题仍然存在。 我认为在这里使用括号是有问题的 - 你有适当的清理代码来取消超时,所以我认为 remove 正在传递的[] 是安全的到useEffect 【参考方案1】:

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

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"
    
)

【讨论】:

这对我不起作用【参考方案2】:

TL;DR

这是一个CodeSandBox 示例

其他答案当然有效,我只是想分享一个我想出的解决方案。 我构建了这个hook,它的工作原理与 React 的 useState 类似,但只有在安装组件时才会设置状态。我觉得它更优雅,因为您不必在组件中使用 isMounted 变量来搞乱!

安装:

npm install use-state-if-mounted

用法:

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

您可以在钩子的npm page 上找到更多高级文档。

【讨论】:

它不起作用,来自 npm 站点:这个“解决方案”不能避免泄漏。即使 AbortController 似乎也不是防止内存泄漏的灵丹妙药?。 遗憾的是,这并不能解决我的问题【参考方案3】:

如果你从 axios 获取数据(使用钩子)并且错误仍然发生,只需将 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;
    ;
, []);

【讨论】:

为什么要在顶部加上isRendered=useRef(false)? 这个不需要let isRendered = useRef(false); isRendered.current 【参考方案4】:

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

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>;

这里是an alternative 和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>;

您可以在此article 中找到有关此模式的更多信息。这是 GitHub 上 React 项目中的 an issue,展示了这个解决方案。

【讨论】:

这是个好方法,但是如何从外部触发更新呢?间隔怎么用? 我没听懂这个问题,你能提供更多的上下文吗? @RTW 你不能在useEffect 中移动你的updateNotSeenAmount 函数? @AndriiGolubenko 我可以,但我在外面也需要它。添加示例。 @RTW 我已经用使用useRef 的替代解决方案更新了答案。

以上是关于反应钩子。无法对未安装的组件执行 React 状态更新的主要内容,如果未能解决你的问题,请参考以下文章

React-Apollo - 查询 - renderProp:反应警告:无法对未安装的组件执行反应状态更新

登录页面后反应路由器 v6 不会切换到主页:警告:无法对未安装的组件执行 React 状态更新

警告:无法对未安装的组件(多个组件)执行 React 状态更新

Expo React 警告:无法对未安装的组件执行 React 状态更新。这是一个无操作,但它表明您的应用程序中存在内存泄漏

错误:无法对未安装的组件执行 React 状态更新

无法对未安装的组件执行 React 状态更新。内存泄漏