0

0

React组件中异步数据获取与状态更新指南

花韻仙語

花韻仙語

发布时间:2025-11-23 14:28:11

|

790人浏览过

|

来源于php中文网

原创

React组件中异步数据获取与状态更新指南

本文将深入探讨在react组件中处理异步数据获取并正确更新ui的常见问题与解决方案。我们将通过一个实际案例,详细分析如何利用react的状态管理机制(`usestate`)、副作用钩子(`useeffect`)以及恰当的数据结构来确保组件在数据加载完成后能够正确地渲染最新信息,特别是在处理多个异步请求并根据结果进行筛选时。

理解React的渲染机制与状态管理

在React中,组件的UI更新(即重新渲染)通常由其状态(state)或属性(props)的变化触发。直接修改组件内部的普通变量并不会导致组件重新渲染。这是许多初学者在处理异步数据时常遇到的一个陷阱。

本教程将围绕一个具体场景展开:从外部API获取多个资金池的APY(年化收益率)数据,然后找出APY最高的“特色资金池”,并将其标题展示在UI上。

初始代码分析与问题诊断

原始代码尝试在一个useEffect钩子中执行多个API请求,并在所有请求完成后计算出最高APY的资金池。然而,它遇到了一个核心问题:数据在控制台打印正确,但UI并未更新。

// 原始代码片段(简化)
export const FeaturedPool = () => {
  let poolDetails: PoolInfo | undefined; // 普通变量,非状态
  // ... 其他状态和逻辑 ...

  useEffect(() => {
    // ... 异步请求和计算逻辑 ...
    // poolDetails = POOLS.find(...) // 直接修改普通变量
    // console.log(poolDetails?.title); // 控制台可见
    setLoading(false); // 仅更新了loading状态
  }, []);

  return (
    <>{loading ? 

Loading...

:

Loaded {poolDetails?.title}

} // poolDetails未触发渲染 ); };

问题根源分析:

  1. 非状态变量导致UI不更新: poolDetails被定义为一个普通的let变量。在useEffect内部对其赋值,并不会通知React组件需要重新渲染。React只对通过useState或useReducer管理的状态变化做出响应。
  2. poolsArray的数据结构不当: 原始代码将poolsArray定义为一个空对象{},并尝试通过poolsArray[pool.targetedAPYId] = result来存储APY。虽然JavaScript对象可以这样使用,但当需要迭代或根据特定条件更新数组中的元素时,一个包含对象的数组(例如[{ targetedAPYId: 'id1', apyReward: '10.23' }])会更具可读性和操作性。
  3. 类型断言滥用: 过多的@ts-ignore表明代码中存在类型不匹配或类型推断问题,这通常是设计问题的信号。

解决方案:正确利用React状态与副作用

为了解决上述问题,我们需要对代码进行重构,核心思想是:任何需要在UI中反映的数据变化都必须通过React的状态管理来实现。

1. 引入状态管理

首先,我们需要为“特色资金池”引入一个状态变量,以便其值更新时能触发组件重新渲染。

import React, { useEffect, useState } from 'react';

// 定义用于存储临时APY数据的类型
export type PoolData = {
  targetedAPYId?: string; // 修正为可选,以防万一
  apyReward: string;
};

// 原始的PoolInfo类型,确保其定义可用
export type PoolInfo = {
  id: string;
  title: string;
  description: string;
  icon: string;
  score: number;
  risk: string;
  apyRange: string;
  targetedAPYId?: string;
  targetedAPY: string;
  tvlId?: string;
  strategy: string;
  vaultAddress: string;
  strategyAddress: string;
  zapAddress: string;
  isRetired?: boolean;
  stableCoins?: boolean;
  wantToken: string;
  isOld?: boolean;
  details?: string;
  benefits?: string[];
  promptTokens?: Token[];
};

// 假设POOLS是从外部导入的常量数组
declare const POOLS: PoolInfo[]; // 声明POOLS变量,实际项目中应从文件导入

export const FeaturedPool = () => {
  const [loading, setLoading] = useState(true);
  // 使用useState来管理featuredPool,初始值为undefined
  const [featuredPool, setFeaturedPool] = useState(undefined);

  // ... useEffect 逻辑 ...
};

2. 优化数据结构与异步处理

在useEffect内部,我们需要更合理地存储和更新每个资金池的APY数据。

小鸽子助手
小鸽子助手

一款集成于WPS/Word的智能写作插件

下载
  useEffect(() => {
    let counter = 0;
    // 将poolsArray定义为PoolData对象的数组
    let poolsArray: PoolData[] = [];

    // 过滤并遍历符合条件的资金池
    POOLS?.filter((x) => x.stableCoins)?.forEach((pool) => {
      // 在发起请求前,为每个资金池在poolsArray中创建一个占位符
      poolsArray.push({ targetedAPYId: pool.targetedAPYId, apyReward: "" });

      fetch("https://yields.llama.fi/chart/" + pool.targetedAPYId)
        .then((response) => response.json())
        .then((res) => {
          // 确保数据存在且格式正确
          const result = res.data.at(-1)?.apyReward?.toFixed(2);

          if (result) {
            // 找到对应的池子并更新其APY
            poolsArray.forEach((poolItem) => {
              if (poolItem.targetedAPYId === pool.targetedAPYId) {
                poolItem.apyReward = result;
              }
            });
          }

          counter++;
          // 当所有请求都完成时(这里假设有3个稳定币池)
          if (counter === POOLS.filter((x) => x.stableCoins).length) { // 更健壮的计数方式
            // 从poolsArray中提取所有APY值,并转换为数字进行比较
            const arr = poolsArray.map((poolItem) => parseFloat(poolItem.apyReward));
            const max = Math.max(...arr);

            // 找到最高APY对应的targetedAPYId
            const poolKey = poolsArray.find((poolItem) => parseFloat(poolItem.apyReward) === max)?.targetedAPYId;

            if (poolKey) {
              // 根据targetedAPYId找到完整的PoolInfo对象
              const foundPool = POOLS.find((p) => p.targetedAPYId === poolKey);
              // 更新featuredPool状态,这将触发组件重新渲染
              setFeaturedPool(foundPool);
            }

            // 数据加载完成,更新loading状态
            setLoading(false);
          }
        })
        .catch(error => {
          console.error("Error fetching APY data:", error);
          // 即使有错误也增加计数,防止loading状态永远不结束
          counter++;
          if (counter === POOLS.filter((x) => x.stableCoins).length) {
            setLoading(false);
          }
        });
    });
  }, []); // 空依赖数组表示只在组件挂载时运行一次

关键改进点:

  • poolsArray作为PoolData[]: 存储APY数据时,每个元素是一个包含targetedAPYId和apyReward的对象,方便查找和更新。
  • forEach遍历和更新: 使用forEach遍历POOLS,并在内部发起fetch请求。每个请求成功后,通过再次遍历poolsArray来更新对应项的apyReward。
  • 动态计数器: 将if (counter === 3)改为if (counter === POOLS.filter((x) => x.stableCoins).length),使其更具通用性,无论有多少个稳定币池都能正确判断所有请求是否完成。
  • 类型安全: 移除不必要的@ts-ignore,并引入PoolData类型来增强代码的可读性和可维护性。
  • 错误处理: 添加.catch()块以捕获API请求中的潜在错误。

3. 更新JSX渲染逻辑

最后,修改JSX部分,使其使用新的featuredPool状态变量进行渲染。

  return (
    <>
      {loading ? 

Loading...

:

Loaded {featuredPool?.title}

} ); };

完整示例代码

结合上述所有修改,以下是完整的FeaturedPool组件代码:

import React, { useEffect, useState } from 'react';

// 假设POOLS是从外部导入的常量数组
// 实际项目中,POOLS可能来自一个单独的常量文件,例如:
// import { POOLS } from '../constants/pools';
declare const POOLS: PoolInfo[]; // 声明POOLS变量,实际项目中应从文件导入

// 定义用于存储临时APY数据的类型
export type PoolData = {
  targetedAPYId?: string; // targetedAPYId在PoolInfo中是可选的,这里保持一致
  apyReward: string;
};

// PoolInfo类型定义,通常从一个公共类型文件导入
export type PoolInfo = {
  id: string;
  title: string;
  description: string;
  icon: string;
  score: number;
  risk: string;
  apyRange: string;
  targetedAPYId?: string;
  targetedAPY: string;
  tvlId?: string;
  strategy: string;
  vaultAddress: string;
  strategyAddress: string;
  zapAddress: string;
  isRetired?: boolean;
  stableCoins?: boolean;
  wantToken: string;
  isOld?: boolean;
  details?: string;
  benefits?: string[];
  promptTokens?: Token[];
};

// 假设Token类型也已定义或导入
declare type Token = {
  // ... Token的属性 ...
};

export const FeaturedPool = () => {
  const [loading, setLoading] = useState(true);
  const [featuredPool, setFeaturedPool] = useState(undefined);

  useEffect(() => {
    let counter = 0;
    // 确保POOLS存在且可迭代
    const stablePools = POOLS?.filter((x) => x.stableCoins) || [];
    const totalStablePools = stablePools.length;

    // 如果没有稳定币池,直接结束加载
    if (totalStablePools === 0) {
      setLoading(false);
      return;
    }

    let poolsArray: PoolData[] = [];

    stablePools.forEach((pool) => {
      poolsArray.push({ targetedAPYId: pool.targetedAPYId, apyReward: "" });

      fetch("https://yields.llama.fi/chart/" + pool.targetedAPYId)
        .then((response) => {
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          return response.json();
        })
        .then((res) => {
          // 确保数据结构符合预期
          const latestData = res.data?.at(-1);
          const result = latestData?.apyReward?.toFixed(2);

          if (result) {
            poolsArray.forEach((poolItem) => {
              if (poolItem.targetedAPYId === pool.targetedAPYId) {
                poolItem.apyReward = result;
              }
            });
          } else {
            console.warn(`No APY reward data found for pool: ${pool.targetedAPYId}`);
          }
        })
        .catch((error) => {
          console.error(`Error fetching APY data for ${pool.targetedAPYId}:`, error);
          // 在错误发生时,将该池的APY设为0或一个默认值,以避免影响后续的max计算
          poolsArray.forEach((poolItem) => {
            if (poolItem.targetedAPYId === pool.targetedAPYId) {
              poolItem.apyReward = "0.00"; // 或者其他错误处理策略
            }
          });
        })
        .finally(() => { // 无论成功或失败,都在finally中增加计数
          counter++;
          if (counter === totalStablePools) {
            // 确保所有APY值都被解析为数字进行比较
            const arr = poolsArray.map((poolItem) => parseFloat(poolItem.apyReward || "0"));
            const max = Math.max(...arr);

            // 找到最高APY对应的targetedAPYId,注意处理多个池子APY相同的情况
            // 这里选择第一个匹配的
            const poolKey = poolsArray.find((poolItem) => parseFloat(poolItem.apyReward || "0") === max)?.targetedAPYId;

            if (poolKey) {
              const foundPool = POOLS.find((p) => p.targetedAPYId === poolKey);
              setFeaturedPool(foundPool);
            } else {
              console.warn("Could not find a pool with the highest APY.");
            }
            setLoading(false);
          }
        });
    });
  }, []); // 依赖数组为空,确保只在组件挂载时运行一次

  return (
    <>
      {loading ? 

Loading...

:

Loaded {featuredPool?.title}

} ); };

注意事项与最佳实践

  1. React状态是UI更新的唯一触发器: 永远记住,在React中,只有通过useState、useReducer或props变化才能触发组件重新渲染。直接修改普通变量不会生效。
  2. useEffect的依赖数组: 确保useEffect的依赖数组正确。空数组[]表示只在组件挂载时运行一次,适用于初始化数据获取。
  3. 数据结构选择: 根据数据的用途选择合适的数据结构。对于需要根据ID查找和更新的列表数据,一个包含对象的数组通常比纯对象更灵活。
  4. 异步操作的完整性: 在处理多个异步请求时,需要一个机制来判断所有请求是否都已完成(例如通过计数器或Promise.all)。
  5. 错误处理: 在API请求中添加.catch()块来处理网络错误或API返回的错误,并考虑如何在UI中反映这些错误。
  6. 类型安全: 充分利用TypeScript的类型定义来增强代码的健壮性和可读性,减少@ts-ignore的使用。
  7. 加载状态: 使用loading状态变量在数据加载期间向用户提供反馈,提升用户体验。
  8. 更高级的异步处理: 对于更复杂的多个并发请求场景,可以考虑使用Promise.all来等待所有Promise完成,这通常比手动计数器更简洁和健壮。

通过遵循这些原则,您可以有效地在React组件中管理异步数据流,并确保UI能够准确、及时地响应数据变化。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

552

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

731

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

656

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

551

2023.09.20

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

2

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.6万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.0万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号