在 React Hooks 中获取 API 数据


prtyaa
prtyaa 2023-12-26 17:31:02 64884
分类专栏: 资讯

对于一个复杂的 React 项目,为了维护方便我们会把与后端 API 交互的逻辑封装在一个单独的 .js 文件中,例如 dataProvider.js。

如下代码示例使用了 axios 从 algolia 获取含有指定关键字的文章列表:

// dataProvider.js
import axios from "axios";

export function getDataFromServer(query) {
  return axios("https://hn.algolia.com/api/v1/search?query=" + query);
}

然后在组件中可以导入此文件,我们在 useEffect 中调用上述的 getDataFromServer 函数,并使用 useState hook 更新组件使用的状态。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
// 导入 dataProvider.js
import { getDataFromServer } from "./dataProvider";

function SearchResults() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("react"); // 初始关键字

  useEffect(async () => {// <= 使用 useEffect Hook,注意这里的关键字 async
    getDataFromServer(query).then((reponseData) => {
      setData(reponseData.data);
    });
  }, [query]);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<SearchResults />, rootElement);

组件看起来可以正常运行,不过在开发者工具控制台中会看到如下警告信息:

Warning: An Effect function must not return anything besides a function, which is used for clean-up.

It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, you may write an async function separately and then call it from inside the effect:

async function fetchComment(commentId) {
  // You can await here
}

useEffect(() => {
  fetchComment(commentId);
}, [commentId]);

In the future, React will provide a more idiomatic solution for data fetching that doesn't involve writing effects manually.
    in SearchResults (at src/index.js:31)

React 作为一个成熟的库给出详细的调试信息,它告诉我们在 Effect 函数如果有返回值的话,只能是一个用于清理副作用的函数。而我们的代码用了 async 关键字,这是不允许的。同时也给了解决办法,可以先写一个单独的 async 函数,然后在 effect 函数内调用它,例如:

async function fetchComment(commentId) {
  // You can await here
}

useEffect(() => {
  fetchComment(commentId);
}, [commentId]);

按照它的指引,我们可以改写 effect 函数如下:

useEffect(() => {
  async function fetchData() {
    const result = await getDataFromServer(query);
    setData(result.data);
  }
  fetchData();
}, [query]);

看起来问题解决了。

不过这里有一个隐藏的问题,如果我们在 effect 函数里添加日志就会发现端倪:

useEffect(() => {
  async function fetchData() {
    console.log("fetch data for keyword:", query);
    const result = await getDataFromServer(query);
    console.log("set data for ", query);
    setData(result.data);
  }
  fetchData();
}, [query]);

如果我们在输入框内输入“1234567890”,有可能输出如下:

fetch data for keyword: 1 
fetch data for keyword: 12 
fetch data for keyword: 123 
set data for keyword 12 
fetch data for keyword: 1234 
set data for keyword 123 
set data for keyword 1 
fetch data for keyword: 12345 
set data for keyword 1234 
fetch data for keyword: 123456 
set data for keyword 12345 
fetch data for keyword: 1234567 
set data for keyword 123456 
set data for keyword 1234567 
fetch data for keyword: 12345678 
set data for keyword 12345678 
fetch data for keyword: 123456789 
fetch data for keyword: 1234567890 
set data for keyword 123456789 
set data for keyword 1234567890 

从日志中可以看到发起 API 查询时和输入的顺序保持一致,但是更新状态时却出现了不一致。例如关键字 “123” 的查询结果早于关键字 "1" 的设置了。

原因不难理解:虽然请求可以依次发出,但是网络请求收到网速等因素影响,无法保证响应依次返回。

要解决这个问题,可以判断当前的 API 请求是否已被忽略,如果已忽略则及时响应返回也不再更新状态。

例如:

useEffect(() => {
  let ignore = false; // 当前 API 请求是否被忽略,默认不忽略
  async function fetchData() {
    console.log("fetch data for keyword:", query);
    const result = await getDataFromServer(query);
    if (!ignore) {
      // 判断当前 API 请求是否仍有效果
      console.log("set data for keyword", query);
      setData(result.data);
    } else {
      console.log("ignore data for keyword", query);
    }
  }
  fetchData();
  return () => {
    // 如果发生 clean up ,则忽略本次 API 请求
    ignore = true;
  };
}, [query]);

这里利用了 JavaScript 的闭包特性保存 API 请求的有效性,同时使用了 useEffect 的 cleanup 功能更新它的有效性。如果一个新的 API 请求发出,则上次的 API 请求的 ignore 会置为 true,即使响应返回也不会更新应用的状态了。

再次输入“1234567890”,可能的输出如下:

fetch data for keyword: 1 
fetch data for keyword: 12 
fetch data for keyword: 123 
ignore data for keyword 12 
fetch data for keyword: 1234 
fetch data for keyword: 12345 
ignore data for keyword 1 
fetch data for keyword: 123456 
ignore data for keyword 123 
fetch data for keyword: 1234567 
ignore data for keyword 123456 
ignore data for keyword 1234 
fetch data for keyword: 12345678 
ignore data for keyword 1234567 
ignore data for keyword 12345 
fetch data for keyword: 123456789 
ignore data for keyword 12345678 
fetch data for keyword: 1234567890 
ignore data for keyword 123456789 
set data for keyword 1234567890

可以看到 setData 函数只为最后输入的关键字执行了一次,其他的均被忽略。问题解决了。

继续优化

至此应用状态更新次序的问题解决了,不过还有一个小问题,那就是虽然忽略了状态不必要的更新,但 API 请求依然每次输入更新时都发出了,这些请求对于服务器端来说造成不必要的压力,我们可以继续优化,取消那些无谓的请求。

Axios 自 v0.22.0 开始支持 AbortController 可以取消网络请求。它的用法如下:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()

我们首先修改 dataProvider:

// dataProvider.js
import axios from "axios";

export function getDataFromServer(query, requestOptions) {
  return axios(
    "https://hn.algolia.com/api/v1/search?query=" + query,
    requestOptions // 允许使用 axios 配置项
  );
}

完善 effect 函数:

useEffect(() => {
  const controller = new AbortController();
  let ignore = false;
  async function fetchData() {
    console.log("fetch data for keyword:", query);
    const result = await getDataFromServer(query, {
      signal: controller.signal // <= 传入 axios 的 signal 配置项
    });
    if (!ignore) {
      console.log("set data for keyword", query);
      setData(result.data);
    } else {
      console.log("ignore data for keyword", query);
    }
  }
  fetchData();
  return () => {
    // 如果发生 clean up ,则忽略本次 API 请求
    ignore = true;
    controller.abort(); // <= cleanup 时取消 API 请求
  };
}, [query]);

此时打印出的日志:

fetch data for keyword: 1 
fetch data for keyword: 12 
fetch data for keyword: 123 
fetch data for keyword: 1234 
fetch data for keyword: 12345 
fetch data for keyword: 123456 
fetch data for keyword: 1234567 
fetch data for keyword: 12345678 
fetch data for keyword: 123456789 
fetch data for keyword: 1234567890 
set data for keyword 1234567890 

 

网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。

本文链接:https://www.xckfsq.com/news/show.html?id=30928
赞同 0
评论 0 条
prtyaaL0
粉丝 1 发表 2554 + 关注 私信
上周热门
银河麒麟添加网络打印机时,出现“client-error-not-possible”错误提示  1323
银河麒麟打印带有图像的文档时出错  1236
银河麒麟添加打印机时,出现“server-error-internal-error”  1022
统信桌面专业版【如何查询系统安装时间】  951
统信操作系统各版本介绍  944
统信桌面专业版【全盘安装UOS系统】介绍  902
麒麟系统也能完整体验微信啦!  889
统信【启动盘制作工具】使用介绍  499
统信桌面专业版【一个U盘做多个系统启动盘】的方法  440
信刻全自动档案蓝光光盘检测一体机  386
本周热议
我的信创开放社区兼职赚钱历程 40
今天你签到了吗? 27
信创开放社区邀请他人注册的具体步骤如下 15
如何玩转信创开放社区—从小白进阶到专家 15
方德桌面操作系统 14
我有15积分有什么用? 13
用抖音玩法闯信创开放社区——用平台宣传企业产品服务 13
如何让你先人一步获得悬赏问题信息?(创作者必看) 12
2024中国信创产业发展大会暨中国信息科技创新与应用博览会 9
中央国家机关政府采购中心:应当将CPU、操作系统符合安全可靠测评要求纳入采购需求 8

添加我为好友,拉您入交流群!

请使用微信扫一扫!