【问题标题】:React Native useEffect with async call results in stale stateReact Native useEffect 与异步调用导致陈旧状态
【发布时间】:2022-01-08 14:21:51
【问题描述】:

我在这里有一个简化的反应原生应用程序,它可以进行网络调用并在加载时设置一个标志。有一个按钮 onPress 处理程序调用另一个方法 doSomething,根据 vscode 中的穷举-deps 插件,useCallback 中的两个方法和依赖数组都是正确的。

当应用程序加载时,我可以看到 isInitialized 标志设置为 true,但是之后按下按钮显示标志在 doSomething 方法中仍然为 false。在这种情况下,似乎 useCallback 方法没有根据它们的依赖数组重新生成。

import React, {useEffect, useState, useCallback} from 'react';
import { Text, View, TouchableOpacity } from 'react-native';


export default function App() {
  const [isInitialized, setIsInitialized] = useState(false);

  useEffect(() => {
    fetch("http://www.google.com").then(() => setIsInitialized(true) );
  }, []);

  const onPress = useCallback(() => {
    doSomething();
  }, [doSomething]);

  const doSomething = useCallback(() => {
    console.log("doSomething", { isInitialized });
  }, [isInitialized]);
  
  return (
    <View style={{flex:1, justifyContent:"center", alignItems:"center"}}>
      {isInitialized &&
        <Text>Initialized</Text>
      }
      <TouchableOpacity onPress={onPress} style={{padding:30, borderWidth:1}}>
        <Text>Press Me</Text>
      </TouchableOpacity>
    </View>
  );
}

有人能解释一下为什么会这样吗?请注意,陈旧状态仅在网络调用后设置标志时发生,并且仅在使用 useCallback() 的方法之间发生两次跃点时发生。如果按钮 onPress 直接设置为 doSomething,则标志正确显示为 true。

我在整个代码中都以这种方式使用 useCallback,我害怕由于不了解这里发生的事情而在意想不到的地方发现陈旧状态。

【问题讨论】:

    标签: react-native async-await hook use-effect usecallback


    【解决方案1】:

    类似的帖子here。另见the React docs on useCallback

    当您在useCallback 中封装一个函数时,您是在告诉 React 不要更新该函数,除非其中一个依赖项发生了变化。但是,useCallback 中的依赖项更改不会触发组件的重新渲染。由于您的 useEffect 没有依赖项,因此组件将永远不会使用新值重新渲染。

    你有以下代码:

      useEffect(() => {
        fetch("http://www.google.com").then(() => setIsInitialized(true) );
      }, []);
    
      const onPress = useCallback(() => {
        doSomething();
      }, [doSomething]);
    
      const doSomething = useCallback(() => {
        console.log("doSomething", { isInitialized });
      }, [isInitialized]);
    

    这三个函数可以重写为:

      useEffect(() => {
        fetch("http://www.google.com").then(() => setIsInitialized(true) );
      }, []);
    
      useEffect(() => {
        console.log({ isInitialized }):
      }, [isInitialized]);
    
      const doSomething = useCallback((isInitialized) => {
        console.log("doSomething", { isInitialized });
      });
    

    这样,doSomething 将始终有一个新值传递给它。然后你会像这样重写你的 TouchableOpacity:

      <TouchableOpacity onPress={() => doSomething(isInitilized)} style={{padding:30, borderWidth:1}}>
        ...
    

    这样,通过强制在第二个useEffect 中重新渲染组件,可以确保isInitialized 的最新值。

    我不确定您的用例,但 useCallback 应谨慎使用。它的重点是及时冻结一个函数并防止它被重新初始化。仅当您有一个需要大量重新渲染的组件时,这才有价值;如果你只做一个fetch,而fetch 不会发生太多,useCallback 会带来比它为你解决的问题更多的问题。

    【讨论】:

    • 感谢您的回复,我阅读了另一篇文章和文档,但我仍然不明白为什么我的代码不能按所写的那样工作。 fetch之后调用setIsInitiatlized(true),会不会触发重渲染,此时useCallback封装的两个方法会根据依赖变化重新生成?
    • useEffect 触发重新渲染,useCallback 不会。
    • 我不相信 useEffect 触发重新渲染,如果它的任何依赖项已更改,则 useEffect 将在渲染后触发。但是,设置状态变量应该会触发重新渲染。
    【解决方案2】:

    函数 doSomething 在您将其作为依赖项传递给 useCallback 时为 undefined,因此函数不会随 isInitialized 更改。将 doSomething 的声明移到 onPress 上方。到处使用 useCallback 可能不是the best idea,但我不知道你的用例,我希望你衡量性能和收益:)

    【讨论】:

    • 发生这种情况是因为 javascript 中的闭包规则,而不是因为函数在渲染时未定义。如果函数未定义,您将看到与 OP 描述的不同的行为。
    • 显然,方法的顺序确实很重要 - 我切换了 onPress 和 doSomething 方法的位置,它可以工作!从现在开始,我肯定会缩减我对 useCallback 的使用。
    猜你喜欢
    • 2019-11-08
    • 2020-12-16
    • 2022-12-20
    • 2015-08-30
    • 1970-01-01
    • 2021-11-21
    • 2015-11-29
    • 2020-12-04
    • 1970-01-01
    相关资源
    最近更新 更多