侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

如何使用 React hooks 获取 api 接口数据

前端  /  管理员 发布于 6年前   159

在本教程中,我想向你展示如何使用 state 和 effect 钩子在react中获取数据。 你还将实现自定义的 hooks 来获取数据,可以在应用程序的任何位置重用,也可以作为独立节点包在npm上发布。

如果你对 react 的新功能一无所知,可以查看 React hooks 的相关 api 介绍。如果你想查看完整的如何使用 React Hooks 获取数据的项目代码,可以查看 github 的仓库

如果你只是想用 React Hooks 进行数据的获取,直接 npm i use-data-api 并根据文档进行操作。如果你使用他,别忘记给我个star 哦~

注意:将来,React Hooks 不适用于 React 中获取数据。一个名为Suspense的功能将负责它。以下演练是了解React中有关 state 和 Effect hooks 的更多信息的好方法。

使用 React hooks 获取数据

如果您不熟悉React中的数据提取,请查看我在React文章中提取的大量数据。 它将引导您完成使用React类组件的数据获取,如何使用Render Prop 组件和高阶组件来复用这些数据,以及它如何处理错误以及 loading 的。

  import React, { useState } from 'react';    function App() {    const [data, setData] = useState({ hits: [] });      return (      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    );  }    export default App;

App 组件显示了一个项目列表(hits=Hacker News 文章)。状态和状态更新函数来自useState 的 hook。他是来负责管理我们这个 data 的状态的。userState 中的第一个值是data 的初始值。其实就是个解构赋值。

这里我们使用 axios 来获取数据,当然,你也可以使用别的开源库。

import React, { useState, useEffect } from 'react';import axios from 'axios';function App() {  const [data, setData] = useState({ hits: [] });  useEffect(async () => {    const result = await axios(      'https://hn.algolia.com/api/v1/search?query=redux',    );    setData(result.data);  });  return (    <ul>      {data.hits.map(item => (        <li key={item.objectID}>          <a href={item.url}>{item.title}</a>        </li>      ))}    </ul>  );}export default App;

这里我们使用 useEffect 的 effect hook 来获取数据。并且使用 useState 中的 setData 来更新组件状态。

但是如上代码运行的时候,你会发现一个特别烦人的循环问题。effect hook 的触发不仅仅是在组件第一次加载的时候,还有在每一次更新的时候也会触发。由于我们在获取到数据后就进行设置了组件状态,然后又触发了 effect hook。所以就会出现死循环。很显然,这是一个 bug!我们只想在组件第一次加载的时候获取数据 ,这也就是为什么你可以提供一个空数组作为 useEffect 的第二个参数以避免在组件更新的时候也触它。当然,这样的话,也就是在组件加载的时候触发。

  import React, { useState, useEffect } from 'react';  import axios from 'axios';    function App() {    const [data, setData] = useState({ hits: [] });      useEffect(async () => {      const result = await axios(        'https://hn.algolia.com/api/v1/search?query=redux',      );        setData(result.data);    }, []);      return (      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    );  }    export default App;

第二个参数可以用来定义 hook 所依赖的所有变量(在这个数组中),如果其中一个变量发生变化,则就会触发这个 hook 的运行。如果传递的是一个空数组,则仅仅在第一次加载的时候运行。

是不是感觉 ,干了shouldComponentUpdate 的事情

这里还有一个陷阱。在这个代码里面,我们使用 async/await 去获取第三方的 API 的接口数据,根据文档,每一个 async都会返回一个 promise:async 函数声明定义了一个异步函数,它返回一个 AsyncFunction 对象。异步函数是通过事件循环异步操作的函数,使用隐式的 Promise 返回结果 然而,effect hook 不应该返回任何内容,或者清除功能。这也就是为啥你看到这个警告:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. ``

这就是为什么我们不能在useEffect中使用 async的原因。但是我们可以通过如下方法解决:

  import React, { useState, useEffect } from 'react';  import axios from 'axios';    function App() {    const [data, setData] = useState({ hits: [] });      useEffect(() => {      const fetchData = async () => {        const result = await axios(          'https://hn.algolia.com/api/v1/search?query=redux',        );          setData(result.data);      };        fetchData();    }, []);      return (      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    );  }    export default App;

如上就是通过 React hooks 来获取 API 数据。但是,如果你对错误处理、loading、如何触发从表单中获取数据或者如何实现可重用的数据获取的钩子。请继续阅读。

如何自动或者手动的触发 hook? (How to trigger a hook programmatically/manually?)

目前我们已经通过组件第一次加载的时候获取了接口数据。但是,如何能够通过输入的字段来告诉 api 接口我对那个主题感兴趣呢?(就是怎么给接口传数据。这里原文说的有点啰嗦(还有 redux 关键字来混淆视听),我直接上代码吧)...

  ...  function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');      useEffect(() => {      const fetchData = async () => {        const result = await axios(          `http://hn.algolia.com/api/v1/search?query=${query}`,        );          setData(result.data);      };        fetchData();    }, []);      return (      ...    );  }    export default App;
这里我跳过一段,原文实在说的太细了。

缺少一件:当你尝试输入字段键入内容的时候,他是不会再去触发请求的。因为你提供的是一个空数组作为useEffect的第二个参数是一个空数组,所以effect hook 的触发不依赖任何变量,因此只在组件第一次加载的时候触发。所以这里我们希望当 query 这个字段一改变的时候就触发搜索

...function App() {  const [data, setData] = useState({ hits: [] });  const [query, setQuery] = useState('redux');  useEffect(() => {    const fetchData = async () => {      const result = await axios(        `http://hn.algolia.com/api/v1/search?query=${query}`,      );      setData(result.data);    };    fetchData();  }, [query]);  return (    ...  );}export default App;

如上,我们只是把 query作为第二个参数传递给了 effect hook,这样的话,每当 query 改变的时候就会触发搜索。但是,这样就会出现了另一个问题:每一次的query 的字段变动都会触发搜索。如何提供一个按钮来触发请求呢?

function App() {  const [data, setData] = useState({ hits: [] });  const [query, setQuery] = useState('redux');  const [search, setSearch] = useState('redux');  useEffect(() => {    const fetchData = async () => {      const result = await axios(        `http://hn.algolia.com/api/v1/search?query=${search}`,      );      setData(result.data);    };    fetchData();  }, [search]);  return (    <Fragment>      <input        type="text"        value={query}        onChange={event => setQuery(event.target.value)}      />      <button type="button" onClick={() => setSearch(query)}>        Search      </button>      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    </Fragment>  );}

搜索的状态设置为组件的初始化状态,组件加载的时候就要触发搜索,类似的查询和搜索状态易造成混淆,为什么不把实际的 URL 设置为状态而不是搜索状态呢?

function App() {  const [data, setData] = useState({ hits: [] });  const [query, setQuery] = useState('redux');  const [url, setUrl] = useState(    'https://hn.algolia.com/api/v1/search?query=redux',  );  useEffect(() => {    const fetchData = async () => {      const result = await axios(url);      setData(result.data);    };    fetchData();  }, [url]);  return (    <Fragment>      <input        type="text"        value={query}        onChange={event => setQuery(event.target.value)}      />      <button        type="button"        onClick={() =>          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)        }      >        Search      </button>      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    </Fragment>  );}

这是一个使用 effect hook 来获取数据的一个例子,你可以决定 effect hook 所以依赖的状态。一旦你点击或者其他的什么操作 setState 了,那么 effect hook 就会运行。但是这个例子中,只有当你的 url 发生变化了,才会再次去获取数据。

在 Effect Hook 中使用 Loading(Loading Indicator with React Hooks)

这里让我们来给程序添加一个 loading(加载器),这里需要另一个 state

import React, { Fragment, useState, useEffect } from 'react';import axios from 'axios';function App() {  const [data, setData] = useState({ hits: [] });  const [query, setQuery] = useState('redux');  const [url, setUrl] = useState(    'https://hn.algolia.com/api/v1/search?query=redux',  );  const [isLoading, setIsLoading] = useState(false);  useEffect(() => {    const fetchData = async () => {      setIsLoading(true);      const result = await axios(url);      setData(result.data);      setIsLoading(false);    };    fetchData();  }, [url]);  return (    <Fragment>      <input        type="text"        value={query}        onChange={event => setQuery(event.target.value)}      />      <button        type="button"        onClick={() =>          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)        }      >        Search      </button>      {isLoading ? (        <div>Loading ...</div>      ) : (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      )}    </Fragment>  );}export default App;
代码比较简单,不解释了使用 Effect Hook 添加错误处理(Error Handling with React Hooks)

如何在 Effect Hook 中做一些错误处理呢?错误仅仅是一个 state ,一旦程序出现了 error state,则组件需要去渲染一些feedback 给用户。当我们使用 async/await 的时候,我们可以使用try/catch,如下:

import React, { Fragment, useState, useEffect } from 'react';import axios from 'axios';function App() {  const [data, setData] = useState({ hits: [] });  const [query, setQuery] = useState('redux');  const [url, setUrl] = useState(    'https://hn.algolia.com/api/v1/search?query=redux',  );  const [isLoading, setIsLoading] = useState(false);  const [isError, setIsError] = useState(false);  useEffect(() => {    const fetchData = async () => {      setIsError(false);      setIsLoading(true);      try {        const result = await axios(url);        setData(result.data);      } catch (error) {        setIsError(true);      }      setIsLoading(false);    };    fetchData();  }, [url]);  return (    <Fragment>      <input        type="text"        value={query}        onChange={event => setQuery(event.target.value)}      />      <button        type="button"        onClick={() =>          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)        }      >        Search      </button>      {isError && <div>Something went wrong ...</div>}      {isLoading ? (        <div>Loading ...</div>      ) : (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      )}    </Fragment>  );}export default App;

每一次 effect hook 运行的时候都需要重置一下 error state,这是非常有必要的。因为用户可能想再发生错误的时候想再次尝试一下。

说白了,界面给用户反馈更加的友好使用 React 中 Form 表单获取数据(Fetching Data with Forms and React)
function App() {  ...  return (    <Fragment>      <form onSubmit={event => {        setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);        event.preventDefault();      }}>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button type="submit">Search</button>      </form>      {isError && <div>Something went wrong ...</div>}      ...    </Fragment>  );}

为了防止浏览器的 reload,我们这里加了一个event.preventDefalut(),然后别的操作就是正常表单的操作了

自定义获取数据的 hook(Custom Data Fetching Hook)其实就是请求的封装

为了能够提取自定义的请求 hook,除了属于输入框的 query 字段,别的包括 loading 加载器、错误处理函数都要包括在内。当然,你需要确保 App Component 所需的所有字段在你自定义的 hook 中都有返回

const useHackerNewsApi = () => {  const [data, setData] = useState({ hits: [] });  const [url, setUrl] = useState(    'https://hn.algolia.com/api/v1/search?query=redux',  );  const [isLoading, setIsLoading] = useState(false);  const [isError, setIsError] = useState(false);  useEffect(() => {    const fetchData = async () => {      setIsError(false);      setIsLoading(true);      try {        const result = await axios(url);        setData(result.data);      } catch (error) {        setIsError(true);      }      setIsLoading(false);    };    fetchData();  }, [url]);  return [{ data, isLoading, isError }, setUrl];}

现在,我们可以将你的新 hook 继续放到组件中使用

function App() {  const [query, setQuery] = useState('redux');  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();  return (    <Fragment>      <form onSubmit={event => {        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);        event.preventDefault();      }}>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button type="submit">Search</button>      </form>      ...    </Fragment>  );}

通常我们需要一个初始状态。将它简单的传递给自定义 hook 中

import React, { Fragment, useState, useEffect } from 'react';import axios from 'axios';const useDataApi = (initialUrl, initialData) => {  const [data, setData] = useState(initialData);  const [url, setUrl] = useState(initialUrl);  const [isLoading, setIsLoading] = useState(false);  const [isError, setIsError] = useState(false);  useEffect(() => {    const fetchData = async () => {      setIsError(false);      setIsLoading(true);      try {        const result = await axios(url);        setData(result.data);      } catch (error) {        setIsError(true);      }      setIsLoading(false);    };    fetchData();  }, [url]);  return [{ data, isLoading, isError }, setUrl];};function App() {  const [query, setQuery] = useState('redux');  const [{ data, isLoading, isError }, doFetch] = useDataApi(    'https://hn.algolia.com/api/v1/search?query=redux',    { hits: [] },  );  return (    <Fragment>      <form        onSubmit={event => {          doFetch(            `http://hn.algolia.com/api/v1/search?query=${query}`,          );          event.preventDefault();        }}      >        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button type="submit">Search</button>      </form>      {isError && <div>Something went wrong ...</div>}      {isLoading ? (        <div>Loading ...</div>      ) : (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      )}    </Fragment>  );}export default App;

如上,就是我们使用自定义 hook 来获取数据,该 hook 本身对 API 一无所知,它从外部接受所有的参数,但是仅管理重要的字段,比如 data、loading、error handler 等。它执行请求并且返回组件所需要的全部数据。

用于数据获取的 Reducer Hook(Reducer Hook for Data Fetching)

目前为止,我们使用各种 state hook 来管理数据、loading、error handler 等。然而,所有的这些状态,通过他们自己的状态管理,都属于同一个整体,因为他们所关心的数据状态都是请求相关的。正如你所看到的,他们都在 fetch 函数中使用。他们属于同一类型的另一个很好的表现就是在函数中,他们是一个接着一个被调用的(比如:setIsError、setIsLoading)。让我们用一个 Reducer Hook 来将这三个状态结合起来!

一个 Reducer Hook 返回一个状态对象和一个改变状态对象的函数。这个函数就是 dispatch function:带有一个 type 和参数的 action。

其实这些概念跟 redux 一毛一样
import React, {  Fragment,  useState,  useEffect,  useReducer,} from 'react';import axios from 'axios';const dataFetchReducer = (state, action) => {  ...};const useDataApi = (initialUrl, initialData) => {  const [url, setUrl] = useState(initialUrl);  const [state, dispatch] = useReducer(dataFetchReducer, {    isLoading: false,    isError: false,    data: initialData,  });  ...};

Reducer Hook将reducer函数和初始状态对象作为参数。 在我们的例子中,数据,加载和错误状态的初始状态的参数没有改变,但它们已经聚合到一个由 reducer hook 而不是单个state hook 管理的状态对象。

const dataFetchReducer = (state, action) => {  ...};const useDataApi = (initialUrl, initialData) => {  const [url, setUrl] = useState(initialUrl);  const [state, dispatch] = useReducer(dataFetchReducer, {    isLoading: false,    isError: false,    data: initialData,  });  useEffect(() => {    const fetchData = async () => {      dispatch({ type: 'FETCH_INIT' });      try {        const result = await axios(url);        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });      } catch (error) {        dispatch({ type: 'FETCH_FAILURE' });      }    };    fetchData();  }, [url]);  ...};

现在,在获取数据的时候,可以使用 dispathc function 来给reducer传递参数。使用dispatch函数发送的对象具有必需的type属性和可选的payload属性。该类型告诉reducer功能需要应用哪个状态转换,并且reducer可以另外使用有效负载来提取新状态。毕竟,我们只有三个状态转换:初始化提取过程,通知成功的数据提取结果,并通知错误的数据提取结果。

在我们自定义的 hook 中,state 像以前一样返回。但是因为我们有一个状态对象而不是独立状态。 这样,调用useDataApi自定义钩子的人仍然可以访问数据,isLoading和isError:

const useDataApi = (initialUrl, initialData) => {  const [url, setUrl] = useState(initialUrl);  const [state, dispatch] = useReducer(dataFetchReducer, {    isLoading: false,    isError: false,    data: initialData,  });  ...  return [state, setUrl];};

最后还有我们 reducer 函数的实现。它需要作用于三个不同的状态转换,称为FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。 每个状态转换都需要返回一个新的状态对象。 让我们看看如何使用switch case语句实现它:

const dataFetchReducer = (state, action) => {  switch (action.type) {    case 'FETCH_INIT':      return {        ...state,        isLoading: true,        isError: false      };    case 'FETCH_SUCCESS':      return {        ...state,        isLoading: false,        isError: false,        data: action.payload,      };    case 'FETCH_FAILURE':      return {        ...state,        isLoading: false,        isError: true,      };    default:      throw new Error();  }};

现在,每一个 action 都有对应的处理,并且返回一个新的 state。

总之,Reducer Hook确保状态管理的这一部分用自己的逻辑封装。此外,你永远不会遇到无效状态。例如,以前可能会意外地将isLoading和isError状态设置为true。 在这种情况下,UI应该显示什么?现在,reducer函数定义的每个状态转换都会导致一个有效的状态对象。

在 Effect Hook 中 中止数据请求(Abort Data Fetching in Effect Hook)

React中的一个常见问题是,即使组件已经卸载(例如由于使用React Router导航),也会设置组件状态。我之前已经在这里写过关于这个问题的文章,它描述了如何防止在各种场景中为未加载的组件中设置状态。 让我们看看我们如何阻止在数据提取的自定义钩子中设置状态:

const useDataApi = (initialUrl, initialData) => {  const [url, setUrl] = useState(initialUrl);  const [state, dispatch] = useReducer(dataFetchReducer, {    isLoading: false,    isError: false,    data: initialData,  });  useEffect(() => {    let didCancel = false;    const fetchData = async () => {      dispatch({ type: 'FETCH_INIT' });      try {        const result = await axios(url);        if (!didCancel) {          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });        }      } catch (error) {        if (!didCancel) {          dispatch({ type: 'FETCH_FAILURE' });        }      }    };    fetchData();    return () => {      didCancel = true;    };  }, [url]);  return [state, setUrl];};

每一个 Effect Hook 都自带一个清理功能。该功能在组件卸载时运行。清理功能是 hook 返回的一个功能。在我们的例子中,我们使用一个名为 didCancel 的 boolean 来标识组件的状态。如果组件已卸载,则该标志应设置为true,这将导致在最终异步解析数据提取后阻止设置组件状态。

注意:实际上不会中止数据获取 - 这可以通过Axios Cancellation实现 - 但是对于 unmounted 的组件不再执行状态转换。 由于Axios Cancellation在我看来并不是最好的API,因此这个防止设置状态的布尔标志也能完成这项工作。

原文地址:robinwieruch   


  • 上一条:
    Vue仿微信app页面跳转动画
    下一条:
    使用webpack4搭建一个基于Vue的组件库
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 使用 Alpine.js 排序插件对元素进行排序(0个评论)
    • 在js中使用jszip + file-saver实现批量下载OSS文件功能示例(0个评论)
    • 在vue中实现父页面按钮显示子组件中的el-dialog效果(0个评论)
    • 使用mock-server实现模拟接口对接流程步骤(0个评论)
    • vue项目打包程序实现把项目打包成一个exe可执行程序(0个评论)
    • 近期文章
    • 智能合约Solidity学习CryptoZombie第三课:组建僵尸军队(高级Solidity理论)(0个评论)
    • 智能合约Solidity学习CryptoZombie第二课:让你的僵尸猎食(0个评论)
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-06
    • 2017-07
    • 2017-08
    • 2017-09
    • 2017-10
    • 2017-11
    • 2018-03
    • 2018-04
    • 2018-05
    • 2018-06
    • 2018-09
    • 2018-11
    • 2018-12
    • 2019-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2021-04
    • 2021-05
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-09
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-02
    • 2024-03
    • 2024-04
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客