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

IT技术 reactjs react-hooks
2021-04-08 01:10:16

我收到此错误:

无法对卸载的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要修复,请取消 useEffect 清理函数中的所有订阅和异步任务。

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

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

代码笔示例

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
}
4个回答

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

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 项目中的一个问题,展示了这个解决方案。

@RTW 你不能updateNotSeenAmount在里面移动你的函数useEffect吗?
2021-05-22 01:10:16
@AndriiGolubenko 我可以,但我在外面也需要它。添加了示例。
2021-05-29 01:10:16
这是个好方法,但是如何从外部触发更新?如何使用间隔?
2021-06-07 01:10:16
@RTW 我已经使用使用useRef.
2021-06-08 01:10:16
我没有理解这个问题,你能提供更多的背景吗?
2021-06-16 01:10:16

TL; 博士

这是一个CodeSandBox示例

其他答案当然有效,我只是想分享我想出的解决方案。我构建了这个钩子,它的工作原理与 React 的 useState 一样,但只有在组件被挂载时才会 setState。我发现它更优雅,因为您不必在组件中使用 isMounted 变量!

安装 :

npm install use-state-if-mounted

用法 :

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

你可以在钩子npm 页面上找到更高级的文档

它不起作用,来自 npm 站点:此“解决方案”无法避免泄漏。甚至 AbortController 似乎也不是解决内存泄漏的灵丹妙药😰。
2021-06-16 01:10:16
可悲的是,这并没有为我解决它
2021-06-17 01:10:16

如果您从 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;
    };
}, []);
这是不需要的 let isRendered = useRef(false);
2021-05-28 01:10:16
isRendered.current <----- 您必须使用 current 关键字:)
2021-05-31 01:10:16
为什么在顶部添加 isRendered=useRef(false) ?
2021-06-16 01:10:16

这是一个简单的解决方案。这个警告是由于当我们在后台执行一些获取请求时(因为一些请求需要一些时间。)并且我们从该屏幕导航回来然后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"
    }
})
这对我不起作用
2021-06-14 01:10:16