如何使用React Hooks请求数据(精简译文)

2,361 阅读6分钟

原文链接:How to fetch data with React Hooks

前言:这篇文章是Dan推荐阅读的入门文章,作者讲的很详细,但对于有一定基础,急需做封装的人来说,则会显得有些啰嗦。于是乎,我想在这篇文章中,更精简地表达他的观点。

1. 使用React Hooks请求数据

这个组件使用了useState来进行数据管理,通过遍历data中的hits数组来进行数据渲染。当前的hits还是个空数组,所以接下来,我们将使用axios来这个库来请求数据。如果你还没有安装axios这个库,你可以先通过 npm intall axios 这行命令进行安装。(注:这里仅用axios这个库来做例子的展示,需要用什么库其实取决于你自己)

然而第一次写的时候,你可能会直接写出以上的代码。这段代码其实有两个问题,原文太啰嗦了,我就精简一下。第一点,useEffect必须返回一个cleanup函数,或者啥都不返回,如果用async声明这个函数的话,那么其实它返回的是一个Promise对象,这是不被允许的。你可以看到控制台报了这种警告。

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..

如果你安装了hook相关eslint插件的话,还会给你这样的提示

其次,如果我们想要在组件渲染的时候,只请求一次的话,应该在useEffect的第二个参数中,加上空数组,这应该算是常识了,初步修改后的组件如下:

接下来,如果你对错误处理,加载指示器,如何在表单中触发获取数据,以及如何实现一个可重复使用的自定义hook感兴趣的话,就接着往下看。

2.如何以编程/手动的方式去触发hook

在上面的例子中,我们在组件渲染的过程中,触发了一次数据渲染。接下来我们将使用一个input元素,让我们的API请求能够根据input的输入值而发生改变。

接着我们给query进行动态的赋值

但这样就行了吗?当前的话,useEffect中的第二个元素为一个空数组,这就说明了它只在组件渲染后执行一次。如果我们需要根据query值的变化来触发这个effect的话,别忘了在第二个参数中加上对应的依赖,如图所示。

现在我们实现了,当input框中的值发生改变时,就能动态地去获取相关的数据信息,但是,如果我们想手动地触发请求呢?这个时候可以加一个按钮,当我们点击按钮时,再触发请求。

别忘了把url中的query,以及useEffect依赖的query变量替换成search

到这里,你有没有察觉到search和query这两个state其实是有些相似的,这容易让人感到疑惑,所以这里我们可以把search state改成url state。

这样,当你通过setUrl来改变url时,也会发起对应的请求,此时的url和query,所代表的含义和先前的query与search相比,更加容易让人理解了。

3. 在hooks中引入加载指示器(loading)

加loading其实非常简单,只需要多声明一个boolean类型的state即可,代码如下:

4. 在hooks中引入错误处理(error handling)

一般来说,我们在发起请求的时候,都会给这个请求包裹上try catch函数,那么我们引入一个错误标识就十分简单了,只需要在catch函数中,把对应的state设为false即可,代码如下:

5. 在form表单提交时发起请求

在form表单提交时发请求,其实也十分简单,但是有一个注意点就是,记得阻止事件的默认行为,否则在点击的时候你的页面将会被刷新。

6. 封装一个自定义请求数据的hook

除了query 是和input输入框相挂钩的state外,其它与请求相关的state,都可以封装在一个函数内,并return 所有需要在组件使用到的相关变量。

在组件中,我们可以这样去使用封装好的useHakerNewsApi

但是,当前的useHackerNewsApi,它的data和url都是写在hook里面的,如果如果我们初始化的data和url不同,那这个hook不就没用了嘛。其实这也很好解决,我们只需要把data和url变成传进来的参数即可。

这样,一个基本的fetch hook就封装好了,其实还是十分简单的,只要有些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

7. 使用useReducer对hook做出修改

在上面的hook例子中,我们定义了相关的state,但有些state,其实它们之间是存在相互关联的关系的,所以,我们可以想办法,把这些具有关联关系的state,给结合起来。这也是hook开发中一个比较常见的做法。那么useState和useReducer,到底用哪个呢。这里大家可以看看作者的另外一篇文章 useReducer vs useState in React ,作者在这个地方,认为使用useReducer的话,可以更好地使代码具有阅读性。

注意啦,useReducer需要你传入一个reducer function和一个initalState object,这里把isLoading,isError,data这三个相关联的state做了合并,并且当作initalState传入。

接下来,我们可以把hook中相关的逻辑,大致分为三个部分,分别是请求初始化,请求成功,请求失败。如图所示:

别忘了,把return的数组也要改一下,毕竟现在state做了合并,只需要导出一个state就够了,代码如下:

接下来,我们需要在reducer中去 实现 FETCH_INIT,FETCH_SUCCESS ,FETCH_FAILURE 对应的函数。实现上其实也不难,如果你对下面的代码有疑惑,那么说明你对useReducer还不够了解,需要再去官方文档补一补知识。

8. 如何中止数据请求

在写代码的过程中,如果在页面卸载的时候,依然去触发相关的setState操作,那么可能就会导致内存泄露。所以,为了防止组件在卸载的过程中,不再去触发对应的操作,我们可以对代码进行优化,代码如下:

完整代码已经被作者上传到github上了,可访问 github.com/the-road-to… 进行查看。