使用useEffect获取数据时避免使用旧数据

2024-02-26

我的问题是,当自定义挂钩使用useEffect with useState(例如,为了获取数据),在依赖项更改之后但在 useEffect 被触发之前,自定义挂钩会返回过时的数据(来自状态)。

您能建议一种正确/惯用的方法来解决这个问题吗?


我正在使用 React 文档和这些文章来指导我:

  • useEffect 完整指南 https://overreacted.io/a-complete-guide-to-useeffect/
  • 如何使用 React Hooks 获取数据? https://www.robinwieruch.de/react-hooks-fetch-data/

我定义了一个函数,它使用useEffect它的目的是包装数据的获取——源代码是 TypeScript 而不是 JavaScript,但这并不重要——我认为这是“按照书本”:

function useGet<TData>(getData: () => Promise<TData>): TData | undefined {

  const [data, setData] = React.useState<TData | undefined>(undefined);

  React.useEffect(() => {
    getData()
      .then((fetched) => setData(fetched));
  }, [getData]);

  // (TODO later -- handle abort of data fetching)

  return data;
}

应用程序根据 URL 路由到各种组件 - 例如,这里是获取并显示用户配置文件数据的组件(当给定像这样的 URL 时https://stackoverflow.com/users/49942/chrisw where 49942是“用户 ID”):

export const User: React.FunctionComponent<RouteComponentProps> =
  (props: RouteComponentProps) => {

  // parse the URL to get the userId of the User profile to be displayed
  const userId = splitPathUser(props.location.pathname);

  // to fetch the data, call the IO.getUser function, passing userId as a parameter
  const getUser = React.useCallback(() => IO.getUser(userId), [userId]);

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet(getUser);

  // use the data to render
  if (!data) {
    // TODO render a place-holder because the data hasn't been fetched yet
  } else {
    // TODO render using the data
  }
}

我认为这是标准的——如果使用不同的 userId 调用组件,那么useCallback将返回不同的值,因此useEffect会再次开火,因为getData在其依赖项数组中声明。

然而,我看到的是:

  1. useGet第一次被调用——它返回undefined因为useEffect尚未触发且数据尚未获取
  2. useEffect触发,获取数据,并且组件使用获取的数据重新渲染
  3. If the userId然后改变useGet再次被召唤——useEffect会开火(因为getData已更改),但尚未触发,所以现在useGet返回陈旧数据(即既不返回新数据也不返回undefined) -- 因此组件会使用过时的数据重新渲染
  4. Soon, useEffect触发,并且组件使用新数据重新渲染

在步骤#3 中使用过时的数据是不可取的。

我怎样才能避免这种情况?有正常/惯用的方式吗?

我在上面引用的文章中没有看到解决此问题的方法。

一个可能的修复(即这似乎有效)是重写useGet函数如下:

function useGet2<TData, TParam>(getData: () => Promise<TData>, param: TParam): TData | undefined {

  const [prev, setPrev] = React.useState<TParam | undefined>(undefined);
  const [data, setData] = React.useState<TData | undefined>(undefined);

  React.useEffect(() => {
    getData()
      .then((fetched) => setData(fetched));
  }, [getData, param]);

  if (prev !== param) {
    // userId parameter changed -- avoid returning stale data
    setPrev(param);
    setData(undefined);
    return undefined;
  }

  return data;
}

...显然该组件是这样调用的:

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet2(getUser, userId);

...但令我担心的是,我在已发表的文章中没有看到这一点——这是必要的也是最好的方法吗?

另外,如果我要明确返回undefined像这样,有没有一种巧妙的方法来测试是否useEffect将要触发,即测试其依赖项数组是否已更改?我必须复制什么吗useEffect通过将旧的 userId 和/或 getData 函数显式存储为状态变量(如useGet2上面的函数)?


为了澄清发生的情况并说明为什么添加“清理钩子”无效,我添加了一个清理钩子useEffect plus console.log消息,所以源代码如下。

function useGet<TData>(getData: () => Promise<TData>): TData | undefined {

  const [data, setData] = React.useState<TData | undefined>(undefined);

  console.log(`useGet starting`);

  React.useEffect(() => {
    console.log(`useEffect starting`);
    let ignore = false;
    setData(undefined);
    getData()
      .then((fetched) => {
        if (!ignore)
          setData(fetched)
      });
    return () => {
      console.log("useEffect cleanup running");
      ignore = true;
    }
  }, [getData, param]);

  console.log(`useGet returning`);
  return data;
}

export const User: React.FunctionComponent<RouteComponentProps> =
  (props: RouteComponentProps) => {

  // parse the URL to get the userId of the User profile to be displayed
  const userId = splitPathUser(props.location.pathname);

  // to fetch the data, call the IO.getUser function, passing userId as a parameter
  const getUser = React.useCallback(() => IO.getUser(userId), [userId]);

  console.log(`User starting with userId=${userId}`);

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet(getUser);

  console.log(`User rendering data ${!data ? "'undefined'" : `for userId=${data.summary.idName.id}`}`);
  if (data && (data.summary.idName.id !== userId)) {
    console.log(`userId mismatch -- userId specifies ${userId} whereas data is for ${data.summary.idName.id}`);
    data = undefined;
  }

  // use the data to render
  if (!data) {
    // TODO render a place-holder because the data hasn't been fetched yet
  } else {
    // TODO render using the data
  }
}

以下是与我上面概述的四个步骤中的每一个相关的运行时日志消息:

  1. useGet第一次被调用——它返回undefined因为useEffect尚未触发且数据尚未获取

    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data 'undefined'
    
  2. useEffect触发,获取数据,并且组件使用获取的数据重新渲染

    useEffect starting
    mockServer getting /users/5/unknown
    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data for userId=5
    
  3. If the userId然后改变useGet再次被召唤——useEffect会开火(因为getData已更改),但尚未触发,所以现在useGet返回陈旧数据(即既不返回新数据也不返回undefined) -- 因此组件会使用过时的数据重新渲染

    User starting with userId=1
    useGet starting
    useGet returning
    User rendering data for userId=5
    userId mismatch -- userId specifies 1 whereas data is for 5
    
  4. Soon, useEffect触发,并且组件使用新数据重新渲染

    useEffect cleanup running
    useEffect starting
    UserProfile starting with userId=1
    useGet starting
    useGet returning
    User rendering data 'undefined'
    mockServer getting /users/1/unknown
    User starting with userId=1
    useGet starting
    useGet returning
    User rendering data for userId=1
    

总之,清理确实作为步骤 4 的一部分运行(可能当第二个useEffect已安排),但这仍然太晚了,无法防止在步骤 3 结束时返回陈旧数据。userId变化和第二个之前useEffect已安排。


在回复中在推特上 https://mobile.twitter.com/cwellsx/status/1127751139053789184,@dan_abramov 写道,我的useGet2解决方案或多或少是规范的:

如果您在渲染内部执行 setState [并且在useEffect] 为了摆脱陈旧状态,它不应该生成用户可观察的中间渲染。它将同步安排另一个重新渲染。所以你的解决方案应该足够了。

这是派生状态的惯用解决方案,在您的示例中,状态是从 ID 派生的。

如何实现 getDerivedStateFromProps? https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

(从长远来看,将会有一种不同的数据获取解决方案,不涉及效果或设置状态。但我正在描述我们今天所拥有的。)

该链接引用的文章——您可能不需要派生状态 https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html-- 解释问题的根本原因是什么。

它说问题是,期望传入的“受控”状态User(即 userId)以匹配“不受控制”的内部状态(即效果返回的数据)。

更好的做法是依赖其中之一,但不要混合使用。

所以我想我应该在内部和/或数据中返回一个 userId 。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用useEffect获取数据时避免使用旧数据 的相关文章

随机推荐

  • Apache 服务器忽略 .htaccess

    我试图让一个网站在我的测试环境中运行 但不知何故它无法运行 我可以加载正常的索引页面 但是当我想访问 page test 时 它会抛出一个错误 指出该页面不存在 我的日志说 File does not exist home page url
  • 使用 @mock.patch.object 模拟方法时返回值未按预期设置

    我的测试应该会成功 因为我用 mocker return value 给出了返回值 这应该为客户提供一个值 以便函数调用最终出现在 else 语句 客户找到 中 但事实并非如此 据我所知 返回值没有被正确模拟 但为什么呢 Mock from
  • Windows .bat/.cmd 函数库在自己的文件中?

    有构建函数的好方法 http www dostips com DtTutoFunctions php在 DOS bat cmd 脚本中 要模块化某些安装脚本 最好将带有函数库的文件包含到 bat cmd 脚本中 我尝试的是 主脚本 bat
  • BitArray - 移位

    我有一个 System Collections BitArray 数组 3000 个项目 我想将所有位向左移动 1 但是该集合似乎不支持该操作 即 bitArray Thanks 这个简单的代码片段展示了手动执行此操作的方法 的价值bitA
  • 返回空指针异常 - Java Selenium Webdriver

    使用 Java 在 Selenium WebDriver 中运行测试时出现空指针异常 由于某种原因 测试返回 null 即使所有内容都被正确声明 我认为 我在这里错过了什么 做错了什么 给定这段代码 public class HomePag
  • 熊猫通过重置获取累积总和

    Problem 我试图保留连续时间戳 分钟频率 的运行总数 我目前有一种方法可以获取累积和并在两列不匹配的情况下重置它 但它是通过 for 循环完成的 我想知道是否有一种方法可以在没有循环的情况下做到这一点 Code cb arbitrag
  • 为什么 opencsv 在写入文件时将 csv 标头大写

    使用 OpenCSV 4 6 将 Bean 写入 CSV 文件时 所有标题都更改为大写 尽管 bean 有 CsvBindByName 注释 但它正在更改为大写 Java 豆 public class ProjectInfo impleme
  • 如何在 SQLCMD 中抑制连字符

    如何从结果集中抑制连字符 sqlcmd命令 C temp gt sqlcmd d AdventureWorks s Q SET NOCOUNT ON SELECT top 5 FirstName LastName FROM Person C
  • 在 Windows 上将 PPT 转换为 JPG/PNG

    我想在 Windows 环境中以编程方式转换一组图像中的 ppt 演示文稿 每张幻灯片一个 我尝试修改以下将 ppt 转换为 pdf 的代码 完美工作 但没有成功 谁能帮我 多谢 Option Explicit Sub WriteLine
  • Pandas 显示 Excel 文件的额外未命名列

    我正在开发一个使用 pandas 库的项目 其中我需要读取一个包含以下列的 Excel 文件 invoiceid locationid timestamp customerid discount tax total subtotal pro
  • 桌面应用程序.net中的视频通话

    我正在开发一个 wpf 应用程序 该应用程序应该具有视频通话功能 所以我寻求 Skype 为我提供解决方案 我正在使用 Skype4COM 库来做到这一点 我的应用程序中包含了 Skype 的所有朋友 我可以给朋友打电话 甚至可以进行视频通
  • 判断文件中是否存在字符串

    我有一个字符串列表 例如 John John Doe 彼得潘 在 txt 文件中 我想创建一个循环来检查某个名称是否存在 但是 如果我搜索 Peter 并且只存在 Peter Pan 我不希望这是真的 每行都必须完全匹配 哈哈 ep0的回答
  • 警告 C4267“参数”:从“size_t”转换为“DWORD”,可能会丢失数据

    我正在将代码从 32 位 vs2012 迁移到 64 位 vs2015 我在程序中遇到了以下函数调用 CryptHashData hHash BYTE AUTH ENCRYPTION KEY wcslen AUTH ENCRYPTION K
  • 删除 Meshlab 或 vcglib 中的自相交

    如何使用 Meshlab 应用程序或 vcglib 消除网格的自相交 这超出了范围 但 CGAL 中有一些函数 这仍然是实验性的 没有记录 但您可以使用该功能remove self intersections https github co
  • 不包含适合入口点的静态“main”方法

    我今天开始将代码组织到单独的 cs 文件中 为了允许与 UI 一起使用的方法继续这样做 我将在相同的命名空间和公共部分类名下创建 cs 代码 以便这些方法可以可互操作 我的标头在四个文件中看起来像这样 包括调用的主核心文件 public s
  • 内存警告但活动字节较小

    在我的应用程序中 在重复某些操作 选择图片 处理 多次后 我收到级别 1 的内存警告 然后收到级别 2 的内存警告 然后崩溃 泄漏工具没有显示任何泄漏 我还关注 Instruments 中的分配工具 我的实时字节大约为 4 MB 总共分配了
  • sqldf:从数据帧创建表错误:“没有这样的表”。并创建了两张表而不是一张

    我最近升级了 R RSQLite 和 sqldf 以下版本 通常情况下 sqldf create table foo as select from bar db test db 应该在附加的 sqlite 数据库中创建一个名为 foo 的表
  • 在 Reactjs 中迭代数组

    const cal days Sun Mon Tue Wed Thu Fri Sat const cal months Jan Feb March April May June July August Sept Oct Nov Dec co
  • 组合 Group-Object 和 ForEach-Object?

    我正在开发一个名为Merge Xsd可以合并相似的 XML 模式 它需要一个路径列表 加载模式 合并它们 并生成一个XMLDocument作为输出 特定文件名的所有模式都被认为是 相似的 所以我正在做的就是获取特定目录结构中的所有子项 根据
  • 使用useEffect获取数据时避免使用旧数据

    我的问题是 当自定义挂钩使用useEffect with useState 例如 为了获取数据 在依赖项更改之后但在 useEffect 被触发之前 自定义挂钩会返回过时的数据 来自状态 您能建议一种正确 惯用的方法来解决这个问题吗 我正在